provisioning-core/nulib/cli/backup.nu

231 lines
9.4 KiB
Text

#!/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 <subcommand> [args]"
print ""
print "Subcommands:"
print " status Print daemon and policy status (JSON or text)"
print " list <target> [--scope s] [--tag k=v ...]"
print " List snapshots for a component, group, or system target"
print " run <target> [--scope s] Trigger a one-shot backup outside cron"
print " restore <component> Restore a snapshot (use --snapshot, --to)"
print " restore --group <name> Restore a BackupGroup at a timestamp (use --at, --to)"
print " mount <component> Mount a snapshot via FUSE (workstation/standalone)"
print " verify <target> [--level lvl] Verify snapshots (quick|deep|restore-drill|full-dr)"
print " policy validate <path> Validate a Nickel policy file or directory"
print " policy render <path> Render a Nickel policy file as JSON"
print " providers List available BackupProvider definitions"
print " destinations [--check] List destinations; --check probes health"
print " daemon <action> status|reload|drain|start"
print " drill run <component> 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 <path> [<path> ...]"
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 <path>"
exit 2
}
^$bin policy render $target --import-path $import_path
}
_ => {
print "Usage: prvng backup policy <validate|render> ..."
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 <component>"
exit 2
}
let target = if (($sub_args | length) > 1) { $sub_args | get 1 } else { "" }
if ($target | is-empty) {
print "Usage: prvng backup drill run <component>"
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
}
}
}