f530c9a3f7
Extend restore with --dry-run (extract to temp, report, no write), running-alloc protection (refuse without --force; stop+restart with --force), post-restore verification (master key, namespaces, DBs), audit log entry. ---ci--- project: orca phase: 07 milestone: v0.11 status: execute ---/ci---
117 lines
3.9 KiB
Go
117 lines
3.9 KiB
Go
// Package cli: backup.go implements the `orca backup` and `orca restore`
|
|
// subcommands (P04, v0.11 milestone).
|
|
//
|
|
// orca backup --out <path> — create a signed tar.gz of ORCA_HOME
|
|
// orca restore --in <path> — restore a verified backup
|
|
//
|
|
// `backup` reads the master key at paths.MasterKeyPath() and backs up
|
|
// paths.Root() (ORCA_HOME). The tarball + HMAC-SHA256 signature are
|
|
// written to --out and --out+".sig". `restore` verifies the signature
|
|
// before extracting; with --force it overwrites a non-empty target.
|
|
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/backup"
|
|
"git.cloudinit.dev/coreci/orca/internal/paths"
|
|
"git.cloudinit.dev/coreci/orca/internal/secrets"
|
|
)
|
|
|
|
var (
|
|
backupOutPath string
|
|
restoreInPath string
|
|
restoreTargetDir string
|
|
restoreForce bool
|
|
restoreDryRun bool
|
|
)
|
|
|
|
var backupCmd = &cobra.Command{
|
|
Use: "backup",
|
|
Short: "Create a signed tar.gz backup of ORCA_HOME",
|
|
Long: `Create a signed tar.gz backup of ORCA_HOME (P04).
|
|
|
|
Walks ` + "`ORCA_HOME`" + ` recursively, excludes /run/orca/*, *.sock,
|
|
*.db-wal, *.db-shm, packs the rest into a tar.gz, and computes an
|
|
HMAC-SHA256 signature using the cluster master key. The tarball is
|
|
written to --out; the hex-encoded signature to --out + ".sig".`,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
mk, err := secrets.LoadMasterKey(paths.MasterKeyPath())
|
|
if err != nil {
|
|
return fmt.Errorf("load master key: %w", err)
|
|
}
|
|
out := backupOutPath
|
|
if out == "" {
|
|
ts := time.Now().UTC().Format("20060102-150405")
|
|
out = fmt.Sprintf("orca-backup-%s.tar.gz", ts)
|
|
}
|
|
opts := backup.BackupOptions{
|
|
SourceDir: paths.Root(),
|
|
OutputPath: out,
|
|
MasterKey: mk,
|
|
}
|
|
if err := backup.Backup(opts); err != nil {
|
|
return err
|
|
}
|
|
if jsonOutput {
|
|
return printJSON(map[string]string{
|
|
"path": out,
|
|
"sig": out + ".sig",
|
|
})
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "✓ Backup written: %s (sig: %s)\n", out, out+".sig")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var restoreCmd = &cobra.Command{
|
|
Use: "restore",
|
|
Short: "Restore ORCA_HOME from a verified signed backup",
|
|
Long: `Restore ORCA_HOME from a verified signed backup (P04/P07).
|
|
|
|
Verifies the HMAC-SHA256 signature on --in (using the cluster master
|
|
key) before extracting. Reconciles with live state: refuses to clobber
|
|
running allocations unless --force is given (with --force, stops the
|
|
running allocs, extracts, then restarts them from the restored state).
|
|
With --dry-run, extracts to a temp dir and reports what WOULD be
|
|
restored without touching the real ORCA_HOME. Performs post-restore
|
|
verification (master key, namespace dirs, SQLite DBs) and records the
|
|
restore in the audit log.`,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
if restoreInPath == "" {
|
|
return fmt.Errorf("--in is required")
|
|
}
|
|
mk, err := secrets.LoadMasterKey(paths.MasterKeyPath())
|
|
if err != nil {
|
|
return fmt.Errorf("load master key: %w", err)
|
|
}
|
|
target := restoreTargetDir
|
|
if target == "" {
|
|
target = paths.Root()
|
|
}
|
|
opts := RestoreOptions{
|
|
InputPath: restoreInPath,
|
|
TargetDir: target,
|
|
MasterKey: mk,
|
|
Force: restoreForce,
|
|
DryRun: restoreDryRun,
|
|
}
|
|
return runRestore(cmd, opts)
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
backupCmd.Flags().StringVar(&backupOutPath, "out", "", "output tarball path (default: orca-backup-<timestamp>.tar.gz in CWD)")
|
|
restoreCmd.Flags().StringVar(&restoreInPath, "in", "", "input tarball path (required)")
|
|
restoreCmd.Flags().StringVar(&restoreTargetDir, "target", "", "restore target dir (default: ORCA_HOME)")
|
|
restoreCmd.Flags().BoolVar(&restoreForce, "force", false, "overwrite a non-empty target directory and stop+restart running allocs")
|
|
restoreCmd.Flags().BoolVar(&restoreDryRun, "dry-run", false, "extract to a temp dir and report what would be restored without touching ORCA_HOME")
|
|
rootCmd.AddCommand(backupCmd)
|
|
rootCmd.AddCommand(restoreCmd)
|
|
}
|