Merge pull request 'feat(flows): add /flows/server-backup page + ops-agent install script' (#1) from feat/flows-server-backup-page into main
Reviewed-on: #1
This commit is contained in:
commit
0d76fc32ca
5 changed files with 493 additions and 0 deletions
|
|
@ -20,6 +20,11 @@ const FLOWS = [
|
|||
title: 'Update Caddy config',
|
||||
desc: 'Reload Caddy met nieuwe Caddyfile + cert renewal check',
|
||||
},
|
||||
{
|
||||
href: '/flows/server-backup',
|
||||
title: 'Server backup',
|
||||
desc: 'pg_dumpall + restic naar NAS én B2 — handmatige backup of restore-test',
|
||||
},
|
||||
]
|
||||
|
||||
export default async function FlowsIndex() {
|
||||
|
|
|
|||
178
app/flows/server-backup/_components/flow-panel.tsx
Normal file
178
app/flows/server-backup/_components/flow-panel.tsx
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useFlowRun } from '@/hooks/useFlowRun'
|
||||
import StreamingTerminal from '@/components/StreamingTerminal'
|
||||
import ConfirmDialog from '@/components/ConfirmDialog'
|
||||
|
||||
// One panel runs either flow; we switch step-list + description based on the
|
||||
// currently active or last-triggered action. Wording mirrors the existing
|
||||
// /settings/backups → server-backup-section so the two entry points stay
|
||||
// consistent. The actual work in both flows runs out-of-band via systemd
|
||||
// (server-backup.service) — the ops-agent flow just kicks it off and tails
|
||||
// the resulting log / status file.
|
||||
|
||||
type Kind = 'backup' | 'restore'
|
||||
|
||||
type FlowSpec = {
|
||||
flowKey: string
|
||||
buttonLabel: string
|
||||
shortDescription: string
|
||||
steps: string[]
|
||||
confirmTitle: string
|
||||
confirmBody: string
|
||||
}
|
||||
|
||||
const FLOWS: Record<Kind, FlowSpec> = {
|
||||
backup: {
|
||||
flowKey: 'server_backup_full',
|
||||
buttonLabel: 'Backup now',
|
||||
shortDescription:
|
||||
'Volledige server-backup: pg_dumpall van alle databases + restic snapshot naar NAS én Backblaze B2 (Object Lock). Draait dagelijks via timer; deze knop triggert handmatig.',
|
||||
steps: [
|
||||
'trigger_server_backup (systemctl start server-backup.service)',
|
||||
'tail_backup_log_today (live log mee-stream)',
|
||||
'read_backup_status (lees status.json met repo-totalen + duur)',
|
||||
],
|
||||
confirmTitle: 'Trigger server backup',
|
||||
confirmBody:
|
||||
'flow: server_backup_full\n\nSteps:\n 1. trigger_server_backup (systemctl start server-backup.service)\n 2. tail_backup_log_today\n 3. read_backup_status\n\nThe actual work happens in systemd; this flow kicks it off and tails the log.',
|
||||
},
|
||||
restore: {
|
||||
flowKey: 'server_backup_restore_test',
|
||||
buttonLabel: 'Run restore test',
|
||||
shortDescription:
|
||||
'Non-destructieve restore-test: haalt de laatste snapshot uit de NAS-repo terug naar /tmp/restore-test en verifieert dat kritieke files er zijn. Raakt niets in de live stack.',
|
||||
steps: [
|
||||
'trigger_restore_test (restore latest NAS snapshot to /tmp/restore-test/)',
|
||||
'read_backup_status (lees assertions + per-file outcome)',
|
||||
],
|
||||
confirmTitle: 'Run restore test (NAS)',
|
||||
confirmBody:
|
||||
'flow: server_backup_restore_test\n\nSteps:\n 1. trigger_restore_test (restore latest NAS snapshot to /tmp/restore-test/)\n 2. read_backup_status\n\nNon-destructive — restores into /tmp only and asserts critical files exist.',
|
||||
},
|
||||
}
|
||||
|
||||
export default function FlowPanel() {
|
||||
// `displayKind` drives the steps/description card; updated optimistically
|
||||
// when the user presses a button so the displayed flow matches the pending
|
||||
// confirm. `activeKind` only flips once the flow actually starts.
|
||||
const [displayKind, setDisplayKind] = useState<Kind>('backup')
|
||||
const [pendingKind, setPendingKind] = useState<Kind | null>(null)
|
||||
const [activeKind, setActiveKind] = useState<Kind | null>(null)
|
||||
const [completedFlowRunId, setCompletedFlowRunId] = useState<string | null>(null)
|
||||
|
||||
const handleComplete = useCallback((flowRunId: string) => {
|
||||
setCompletedFlowRunId(flowRunId)
|
||||
}, [])
|
||||
|
||||
const flowRun = useFlowRun(handleComplete)
|
||||
|
||||
const handleClickKind = useCallback((kind: Kind) => {
|
||||
setDisplayKind(kind)
|
||||
setPendingKind(kind)
|
||||
}, [])
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
if (pendingKind === null) return
|
||||
const kind = pendingKind
|
||||
setPendingKind(null)
|
||||
setCompletedFlowRunId(null)
|
||||
setActiveKind(kind)
|
||||
flowRun.startFlow(FLOWS[kind].flowKey, false)
|
||||
}, [pendingKind, flowRun])
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
flowRun.reset()
|
||||
setCompletedFlowRunId(null)
|
||||
setActiveKind(null)
|
||||
}, [flowRun])
|
||||
|
||||
const spec = FLOWS[displayKind]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-lg border border-border p-5 space-y-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">{spec.shortDescription}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground font-mono">
|
||||
flow: {spec.flowKey}
|
||||
</p>
|
||||
</div>
|
||||
<ol className="space-y-1">
|
||||
{spec.steps.map((step, i) => (
|
||||
<li key={i} className="flex gap-2 text-xs font-mono text-muted-foreground">
|
||||
<span className="text-border min-w-[1.5rem]">{i + 1}.</span>
|
||||
<span>{step}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => handleClickKind('backup')}
|
||||
disabled={flowRun.status === 'running'}
|
||||
className={`rounded-lg px-4 py-2 text-sm font-medium transition-opacity disabled:opacity-50 ${
|
||||
displayKind === 'backup'
|
||||
? 'bg-foreground text-background hover:opacity-90'
|
||||
: 'border border-border hover:bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
{FLOWS.backup.buttonLabel}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleClickKind('restore')}
|
||||
disabled={flowRun.status === 'running'}
|
||||
className={`rounded-lg px-4 py-2 text-sm font-medium transition-opacity disabled:opacity-50 ${
|
||||
displayKind === 'restore'
|
||||
? 'bg-foreground text-background hover:opacity-90'
|
||||
: 'border border-border hover:bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
{FLOWS.restore.buttonLabel}
|
||||
</button>
|
||||
{flowRun.status !== 'idle' && flowRun.status !== 'running' && (
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{flowRun.status !== 'idle' && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">
|
||||
Output{activeKind ? ` — ${FLOWS[activeKind].buttonLabel}` : ''}
|
||||
</span>
|
||||
{completedFlowRunId && (
|
||||
<Link
|
||||
href={`/audit/${completedFlowRunId}`}
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
View in audit log →
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<StreamingTerminal
|
||||
lines={flowRun.lines}
|
||||
status={flowRun.status}
|
||||
error={flowRun.error}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
open={pendingKind !== null}
|
||||
title={pendingKind ? FLOWS[pendingKind].confirmTitle : ''}
|
||||
commandPreview={pendingKind ? FLOWS[pendingKind].confirmBody : ''}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={() => setPendingKind(null)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
31
app/flows/server-backup/page.tsx
Normal file
31
app/flows/server-backup/page.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import Link from 'next/link'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { getCurrentUser } from '@/lib/session'
|
||||
import FlowPanel from './_components/flow-panel'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function ServerBackupPage() {
|
||||
const user = await getCurrentUser()
|
||||
if (!user) redirect('/login')
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-6">
|
||||
<div className="mx-auto max-w-4xl space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/" className="text-sm text-muted-foreground hover:text-foreground">
|
||||
← Home
|
||||
</Link>
|
||||
<span className="text-muted-foreground">/</span>
|
||||
<Link href="/flows" className="text-sm text-muted-foreground hover:text-foreground">
|
||||
Flows
|
||||
</Link>
|
||||
<span className="text-muted-foreground">/</span>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Server backup</h1>
|
||||
</div>
|
||||
|
||||
<FlowPanel />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -63,6 +63,21 @@ sudo systemctl start server-backup.service
|
|||
journalctl -u server-backup.service -f
|
||||
```
|
||||
|
||||
### Ops-agent wiring (na stap 1-7)
|
||||
|
||||
Voor de **/flows/server-backup**-pagina en **/settings/backups** in het dashboard
|
||||
moet ops-agent ook weten van de wrappers, commands, flow-YAMLs en de
|
||||
NOPASSWD-sudoers-regels. Dat doet een idempotent install-script:
|
||||
|
||||
```bash
|
||||
sudo bash deploy/server-backup/install-flows.sh
|
||||
```
|
||||
|
||||
Wat het regelt (en wat het bewust **niet** doet) staat in de header van het
|
||||
script. Re-run safe; backups van `commands.yml` en `sudoers.d/ops-agent` worden
|
||||
bewaard met `.bak.<timestamp>`-suffix. Daarna is de UI op
|
||||
`/flows/server-backup` direct te gebruiken.
|
||||
|
||||
## Verifiëren
|
||||
|
||||
```bash
|
||||
|
|
|
|||
264
deploy/server-backup/install-flows.sh
Executable file
264
deploy/server-backup/install-flows.sh
Executable file
|
|
@ -0,0 +1,264 @@
|
|||
#!/usr/bin/env bash
|
||||
# Idempotent installer that wires the server-backup flow into ops-agent.
|
||||
#
|
||||
# What this DOES install:
|
||||
# 1. /srv/backups/scripts/wrappers/*.sh (wrapper scripts used by ops-agent)
|
||||
# 2. /etc/ops-agent/flows/server_backup_*.yml (flow YAMLs for full + restore-test)
|
||||
# 3. /etc/ops-agent/commands.yml (appends backup commands if missing)
|
||||
# 4. /etc/sudoers.d/ops-agent (appends wrapper allowlist, visudo-validated)
|
||||
# 5. systemctl restart ops-agent (pick up new commands/flows)
|
||||
# 6. systemctl enable --now server-backup.timer (daily backup)
|
||||
#
|
||||
# What this DOES NOT do (do manually first — see README "Snelle installatie"):
|
||||
# - Create /etc/restic-backup.env (with NAS path, B2 keys, Forgejo container name)
|
||||
# - Create /etc/restic-backup.password
|
||||
# - Initialise the restic repos (NAS + B2)
|
||||
# - Install /srv/backups/scripts/{server-backup.sh,restore-test.sh}
|
||||
# - Install /etc/systemd/system/server-backup.{service,timer}
|
||||
#
|
||||
# Re-run safe: each step checks for prior state and skips. Backups of mutated
|
||||
# files (commands.yml, sudoers) are kept with a .bak.<timestamp> suffix.
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash deploy/server-backup/install-flows.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Resolve repo root from this script's location, so it works regardless of cwd.
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
|
||||
REPO="$(cd -- "$SCRIPT_DIR/../.." &>/dev/null && pwd)"
|
||||
|
||||
WRAPPERS_SRC="$REPO/deploy/server-backup/wrappers"
|
||||
FLOWS_SRC="$REPO/ops-agent/flows.example"
|
||||
COMMANDS_SRC="$REPO/ops-agent/commands.yml.example"
|
||||
|
||||
WRAPPERS_DST=/srv/backups/scripts/wrappers
|
||||
FLOWS_DST=/etc/ops-agent/flows
|
||||
COMMANDS_DST=/etc/ops-agent/commands.yml
|
||||
SUDOERS_DST=/etc/sudoers.d/ops-agent
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "ERROR: run as root (sudo)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
step() { echo; echo "── $* ──"; }
|
||||
ok() { echo " ✓ $*"; }
|
||||
skip() { echo " · $* (already in place)"; }
|
||||
note() { echo " ! $*"; }
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
step "1. Install wrappers to $WRAPPERS_DST"
|
||||
|
||||
mkdir -p "$WRAPPERS_DST"
|
||||
chown root:root "$WRAPPERS_DST"
|
||||
chmod 0750 "$WRAPPERS_DST"
|
||||
|
||||
if [[ ! -d "$WRAPPERS_SRC" ]]; then
|
||||
echo "ERROR: $WRAPPERS_SRC not found — repo state unexpected (expected at $REPO)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for src in "$WRAPPERS_SRC"/*.sh; do
|
||||
name=$(basename "$src")
|
||||
dst="$WRAPPERS_DST/$name"
|
||||
if [[ -f "$dst" ]] && cmp -s "$src" "$dst"; then
|
||||
skip "$name"
|
||||
else
|
||||
install -o root -g root -m 0750 "$src" "$dst"
|
||||
ok "$name installed"
|
||||
fi
|
||||
done
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
step "2. Install flow YAMLs to $FLOWS_DST"
|
||||
|
||||
mkdir -p "$FLOWS_DST"
|
||||
|
||||
for f in server_backup_full.yml server_backup_restore_test.yml; do
|
||||
src="$FLOWS_SRC/$f"
|
||||
dst="$FLOWS_DST/$f"
|
||||
if [[ ! -f "$src" ]]; then
|
||||
echo "ERROR: $src missing — repo state unexpected" >&2
|
||||
exit 3
|
||||
fi
|
||||
if [[ -f "$dst" ]] && cmp -s "$src" "$dst"; then
|
||||
skip "$f"
|
||||
else
|
||||
install -o root -g root -m 0644 "$src" "$dst"
|
||||
ok "$f installed"
|
||||
fi
|
||||
done
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
step "3. Append missing commands to $COMMANDS_DST"
|
||||
|
||||
# Commands we want to ensure exist. Names must match the YAML in commands.yml.example.
|
||||
NEEDED_CMDS=(
|
||||
trigger_server_backup
|
||||
trigger_restore_test
|
||||
tail_backup_log_today
|
||||
read_backup_status
|
||||
restic_snapshots_nas
|
||||
restic_snapshots_b2
|
||||
restic_stats_nas
|
||||
restic_stats_b2
|
||||
)
|
||||
|
||||
if [[ ! -f "$COMMANDS_DST" ]]; then
|
||||
echo "ERROR: $COMMANDS_DST missing — run base ops-agent install first" >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
TS=$(date +%Y%m%d-%H%M%S)
|
||||
cp -p "$COMMANDS_DST" "${COMMANDS_DST}.bak.${TS}"
|
||||
|
||||
# Check which commands are missing
|
||||
missing_cmds=()
|
||||
for cmd in "${NEEDED_CMDS[@]}"; do
|
||||
if grep -qE "^ ${cmd}:" "$COMMANDS_DST"; then
|
||||
skip "command $cmd already in commands.yml"
|
||||
else
|
||||
missing_cmds+=("$cmd")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#missing_cmds[@]} -eq 0 ]]; then
|
||||
skip "no commands to add"
|
||||
rm "${COMMANDS_DST}.bak.${TS}" # no-op edit, drop the backup
|
||||
else
|
||||
# Extract each missing command's YAML block from commands.yml.example.
|
||||
# The block-detection regex MUST include digits — command names like
|
||||
# restic_snapshots_b2 / restic_stats_b2 contain digits, otherwise the
|
||||
# following block (e.g. restic_stats_nas) would swallow them.
|
||||
tmp=$(mktemp)
|
||||
python3 - "$COMMANDS_SRC" "${missing_cmds[@]}" >> "$tmp" <<'PY'
|
||||
import sys, re
|
||||
src_path = sys.argv[1]
|
||||
wanted = sys.argv[2:]
|
||||
with open(src_path) as f:
|
||||
src = f.read()
|
||||
# Top-level command blocks: each starts at column-2 with "<name>:" line.
|
||||
# A block ends at the next sibling-key line (same indentation) or EOF.
|
||||
pattern = re.compile(r"(^ [a-z0-9_]+:[\s\S]*?)(?=^ [a-z0-9_]+:|\Z)", re.M)
|
||||
blocks = {}
|
||||
for m in pattern.finditer(src):
|
||||
block = m.group(1)
|
||||
name_match = re.match(r"^ ([a-z0-9_]+):", block)
|
||||
if name_match:
|
||||
blocks[name_match.group(1)] = block.rstrip() + "\n"
|
||||
exit_code = 0
|
||||
for cmd in wanted:
|
||||
if cmd in blocks:
|
||||
sys.stdout.write("\n" + blocks[cmd])
|
||||
else:
|
||||
sys.stderr.write(f"ERROR: {cmd} not found in {src_path}\n")
|
||||
exit_code = 1
|
||||
sys.exit(exit_code)
|
||||
PY
|
||||
|
||||
if [[ -s "$tmp" ]]; then
|
||||
cat "$tmp" >> "$COMMANDS_DST"
|
||||
rm "$tmp"
|
||||
ok "appended ${#missing_cmds[@]} commands: ${missing_cmds[*]}"
|
||||
note "backup at ${COMMANDS_DST}.bak.${TS}"
|
||||
else
|
||||
rm "$tmp"
|
||||
echo "ERROR: extraction produced empty output — aborting" >&2
|
||||
exit 5
|
||||
fi
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
step "4. Ensure sudoers allows ops-agent to run wrappers"
|
||||
|
||||
WRAPPER_PATHS=(
|
||||
/srv/backups/scripts/wrappers/trigger-backup.sh
|
||||
/srv/backups/scripts/wrappers/trigger-restore-test.sh
|
||||
/srv/backups/scripts/wrappers/read-status.sh
|
||||
/srv/backups/scripts/wrappers/restic-snapshots.sh
|
||||
/srv/backups/scripts/wrappers/restic-stats.sh
|
||||
/srv/backups/scripts/wrappers/restic-check.sh
|
||||
)
|
||||
|
||||
# Build proposed sudoers content: existing file + missing wrapper-NOPASSWD lines.
|
||||
SUDOERS_TMP=$(mktemp /tmp/sudoers-ops-agent.XXXXXX)
|
||||
chmod 0440 "$SUDOERS_TMP"
|
||||
cp "$SUDOERS_DST" "$SUDOERS_TMP"
|
||||
|
||||
added_lines=0
|
||||
for path in "${WRAPPER_PATHS[@]}"; do
|
||||
pattern="NOPASSWD:[[:space:]]*${path//\//\\/}\\b"
|
||||
if grep -qE "$pattern" "$SUDOERS_TMP"; then
|
||||
skip "$(basename "$path") already in sudoers"
|
||||
else
|
||||
echo "ops-agent ALL=(root) NOPASSWD: $path *" >> "$SUDOERS_TMP"
|
||||
ok "added NOPASSWD for $(basename "$path")"
|
||||
added_lines=$((added_lines + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $added_lines -gt 0 ]]; then
|
||||
# Validate with visudo before swapping in — bail loud if invalid (prevents lockout).
|
||||
if visudo -c -f "$SUDOERS_TMP" >/dev/null; then
|
||||
cp -p "$SUDOERS_DST" "${SUDOERS_DST}.bak.${TS}"
|
||||
install -o root -g root -m 0440 "$SUDOERS_TMP" "$SUDOERS_DST"
|
||||
rm "$SUDOERS_TMP"
|
||||
ok "sudoers updated (visudo-validated); backup at ${SUDOERS_DST}.bak.${TS}"
|
||||
else
|
||||
echo "ERROR: visudo validation failed — sudoers not modified" >&2
|
||||
echo " check $SUDOERS_TMP" >&2
|
||||
exit 6
|
||||
fi
|
||||
else
|
||||
rm "$SUDOERS_TMP"
|
||||
skip "sudoers already complete"
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
step "5. Restart ops-agent (reload commands.yml + flows)"
|
||||
|
||||
systemctl restart ops-agent
|
||||
sleep 1
|
||||
if systemctl is-active --quiet ops-agent; then
|
||||
ok "ops-agent restarted ($(systemctl show -p ActiveEnterTimestamp ops-agent --value))"
|
||||
else
|
||||
echo "ERROR: ops-agent failed to start — check 'journalctl -u ops-agent -n 50'" >&2
|
||||
exit 7
|
||||
fi
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
step "6. Enable server-backup.timer"
|
||||
|
||||
if systemctl is-enabled --quiet server-backup.timer; then
|
||||
skip "server-backup.timer already enabled"
|
||||
else
|
||||
systemctl enable server-backup.timer
|
||||
ok "server-backup.timer enabled"
|
||||
fi
|
||||
|
||||
if systemctl is-active --quiet server-backup.timer; then
|
||||
skip "server-backup.timer already active"
|
||||
else
|
||||
systemctl start server-backup.timer
|
||||
ok "server-backup.timer started"
|
||||
fi
|
||||
|
||||
# Show next-firing
|
||||
echo
|
||||
note "next scheduled runs:"
|
||||
systemctl list-timers --no-pager | grep -E "NEXT|server-backup" | head -5
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
step "Done"
|
||||
|
||||
echo
|
||||
echo "Test via the UI:"
|
||||
echo " /flows/server-backup → click 'Run restore test' (non-destructive)"
|
||||
echo
|
||||
echo "Or test via curl on this host:"
|
||||
echo " TOKEN=\$(cat /etc/ops-agent/secret)"
|
||||
echo " curl -sS -H \"Authorization: Bearer \$TOKEN\" \\"
|
||||
echo " -H 'Content-Type: application/json' \\"
|
||||
echo " -X POST http://127.0.0.1:3099/agent/v1/flow \\"
|
||||
echo " --data '{\"flow_key\":\"server_backup_restore_test\",\"dry_run\":false}'"
|
||||
Loading…
Add table
Add a link
Reference in a new issue