#!/usr/bin/env nu # Thin entry for backup commands. Dispatches to the prvng-backup binary # (the Rust backup-manager crate) for all heavy lifting; this script handles # argument parsing, help text, and the `policy validate` shortcut against # workspace declarations. # # See: ADR-010 (backup-architecture), ADR-015 (config-driven via platform-config) export-env { let lib_dirs_raw = ($env.NU_LIB_DIRS? | default "") let current_lib_dirs = if ($lib_dirs_raw | type) == "string" { if ($lib_dirs_raw | is-empty) { [] } else { ($lib_dirs_raw | split row ":") } } else { $lib_dirs_raw } let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib") $env.NU_LIB_DIRS = ([ "/opt/provisioning/core/nulib" "/usr/local/provisioning/core/nulib" ] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] })) } # Resolve the prvng-backup binary. Searches: # 1. $PRVNG_BACKUP_BIN environment override # 2. $PROVISIONING/platform/target/release/prvng-backup # 3. $PROVISIONING/platform/target/debug/prvng-backup # 4. PATH lookup (`which prvng-backup`) # Returns "" when not found; callers handle the absence. def resolve-backup-bin [] { let override = ($env.PRVNG_BACKUP_BIN? | default "") if ($override | is-not-empty) { return $override } let prov = ($env.PROVISIONING? | default "") if ($prov | is-not-empty) { let release = ($prov | path join "platform" "target" "release" "prvng-backup") if ($release | path exists) { return $release } let debug = ($prov | path join "platform" "target" "debug" "prvng-backup") if ($debug | path exists) { return $debug } } let lookup = (which prvng-backup | get path) if ($lookup | is-not-empty) { return ($lookup | first | into string) } "" } # Default Nickel import path. Workspace components import from # 'schemas/lib/...', so the parent of the schemas/ directory is required. def resolve-import-path [] { let override = ($env.NICKEL_IMPORT_PATH? | default "") if ($override | is-not-empty) { return $override } let prov = ($env.PROVISIONING? | default "") if ($prov | is-not-empty) { let candidate = ($prov | path expand) if (($candidate | path join "schemas") | path exists) { return $candidate } } "provisioning" } def print-help []: nothing -> nothing { print "Backup Manager — multi-context backup orchestrator" print "===================================================" print "" print "Usage: prvng backup [args]" print "" print "Subcommands:" print " status Print daemon and policy status (JSON or text)" print " list [--scope s] [--tag k=v ...]" print " List snapshots for a component, group, or system target" print " run [--scope s] Trigger a one-shot backup outside cron" print " restore Restore a snapshot (use --snapshot, --to)" print " restore --group Restore a BackupGroup at a timestamp (use --at, --to)" print " mount Mount a snapshot via FUSE (workstation/standalone)" print " verify [--level lvl] Verify snapshots (quick|deep|restore-drill|full-dr)" print " policy validate Validate a Nickel policy file or directory" print " policy render Render a Nickel policy file as JSON" print " providers List available BackupProvider definitions" print " destinations [--check] List destinations; --check probes health" print " daemon status|reload|drain|start" print " drill run Invoke a verify-recipe drill on demand" print " events tail [--subject s] Tail NATS events for backup.>" print " help Show this message" print "" print "Environment:" print " PRVNG_BACKUP_BIN Override path to the prvng-backup binary" print " NICKEL_IMPORT_PATH Override Nickel --import-path (default: \$PROVISIONING)" print " BACKUP_CONFIG Path to backup-manager.ncl config file" print " BACKUP_LOG Tracing filter (default: 'info')" print "" print "Reference: ADR-010 (architecture), ADR-015 (config-driven), ADR-016 (events)," print " ADR-017 (vault custody)." } def main [ ...args: string --workspace (-w): string = "" --debug (-x) ]: nothing -> nothing { if $debug { $env.PROVISIONING_DEBUG = true } let rest = if (($args | length) > 0) and (($args | first) in ["backup", "bak"]) { $args | skip 1 } else { $args } let sub = if (($rest | length) > 0) { $rest | first } else { "help" } let sub_args = if (($rest | length) > 1) { $rest | skip 1 } else { [] } let bin = (resolve-backup-bin) if ($bin | is-empty) { print "ERROR: prvng-backup binary not found. Set PRVNG_BACKUP_BIN, build with" print " 'cargo build -p backup-manager', or add it to PATH." exit 1 } let import_path = (resolve-import-path) match $sub { "help" | "-h" | "--help" | "h" => { print-help } "policy" => { let action = if (($sub_args | length) > 0) { $sub_args | first } else { "help" } let action_args = if (($sub_args | length) > 1) { $sub_args | skip 1 } else { [] } match $action { "validate" => { let paths = if (($action_args | length) > 0) { $action_args } else { print "Usage: prvng backup policy validate [ ...]" exit 2 } let cmd_args = (["policy", "validate"] | append $paths | append ["--import-path", $import_path]) ^$bin ...$cmd_args } "render" => { let target = if (($action_args | length) > 0) { $action_args | first } else { "" } if ($target | is-empty) { print "Usage: prvng backup policy render " exit 2 } ^$bin policy render $target --import-path $import_path } _ => { print "Usage: prvng backup policy ..." exit 2 } } } "status" => { ^$bin daemon status ...$sub_args } "list" => { ^$bin one-shot list ...$sub_args } "run" => { ^$bin one-shot backup ...$sub_args } "verify" => { ^$bin one-shot verify ...$sub_args } "restore" => { ^$bin one-shot restore ...$sub_args } "mount" => { ^$bin standalone mount ...$sub_args } "providers" => { print "Configured providers (from BackupProvider definitions):" print " restic primary; mount + streaming + encryption" print " kopia opt-in; global dedup, mount less battle-tested" print "" print "See provisioning/catalog/providers/backup/ for definitions." } "destinations" => { # Workspace declarations live in lib/backup_policies.ncl. The binary # will read them in a later iteration; for now we render the file. let prov = ($env.PROVISIONING? | default "") let workspace_root = ($env.WORKSPACE_ROOT? | default "") let candidate = if ($workspace_root | is-not-empty) { $workspace_root | path join "infra" "lib" "backup_policies.ncl" } else { "infra/lib/backup_policies.ncl" } if ($candidate | path exists) { ^$bin policy render $candidate --import-path $import_path } else { print $"Workspace destinations file not found: ($candidate)" print "Set WORKSPACE_ROOT or run from the workspace directory." exit 2 } } "daemon" => { let action = if (($sub_args | length) > 0) { $sub_args | first } else { "status" } let action_args = if (($sub_args | length) > 1) { $sub_args | skip 1 } else { [] } ^$bin daemon $action ...$action_args } "drill" => { let action = if (($sub_args | length) > 0) { $sub_args | first } else { "" } if $action != "run" { print "Usage: prvng backup drill run " exit 2 } let target = if (($sub_args | length) > 1) { $sub_args | get 1 } else { "" } if ($target | is-empty) { print "Usage: prvng backup drill run " exit 2 } print $"Drill mode for ($target) is wired in the CLI surface;" print "the daemon coordinator implementation lands in a subsequent commit." exit 2 } "events" => { let action = if (($sub_args | length) > 0) { $sub_args | first } else { "tail" } print $"NATS events ($action) wired in the CLI surface;" print "the platform-nats integration lands in a subsequent commit." exit 2 } _ => { print $"Unknown backup subcommand: ($sub)" print "Run: prvng backup help" exit 2 } } }