ee3ee241ea
Cohesive batch from a smoke-test session against a real rest-server.
Themed bullets:
* Agent runs as root, sandboxed via systemd. CapabilityBoundingSet
drops to CAP_DAC_READ_SEARCH + restore caps; ProtectSystem=strict
with ReadWritePaths confined to /etc + /var/lib/restic-manager;
NoNewPrivileges blocks escalation. Install script no longer
creates a service user. spec.md §4.2 / §14.1 / §14.3 explain the
rationale (matches UrBackup / Veeam / Bareos defaults; trying to
back up "everything" as an unprivileged user creates silent skips
on /home, /root, /var/lib/* with no upside vs the threat model
the agent already implies).
* Init-repo end-to-end. New JobKind="init" wired through agent
runner, restic.Env.RunInit, server dispatcher, and a UI button
(red "Initialise repo" in the run-now panel). hosts.repo_initialised_at
flips on init success, on backup success, or on a non-empty
snapshots.report. The "Run now" / "Init" / "Retry" branching now
drives both the dashboard host row and the host-detail panel.
Migrations 0004 (column), 0005 (jobs.kind CHECK widened — using
the safe create-new-then-rename pattern; first version corrupted
job_logs.job_id FK), 0006 (cleans up job_logs FK on already-
affected DBs).
* rest-server creds embedded at exec time only. restic.Env gains
RepoUsername; mergeRestCreds() builds the user:pass@-prefixed URL
inside envSlice() and never assigns it back to the struct, so
nothing slog-able ever sees the cleartext form. RedactURL helper
for any future surface that needs to log a URL safely. Both
helpers tested.
* Add-host UX. Repo password is now optional — server mints a
24-byte URL-safe random one and surfaces it once, alongside an
htpasswd snippet ("echo PASS | htpasswd -B -i ... USERNAME") so
the operator pastes one command on the rest-server host and one
on the endpoint. Result page also links the install snippet at
/install/install.sh (was /install.sh — 404'd before) and pipes
to bash (not sh — script uses set -o pipefail and other
bashisms; on Debian/Ubuntu sh is dash).
* Late-subscriber race in JobHub. A fast-failing job could finish
(DB write + Broadcast) before the browser's HX-Redirect → page
load → WS-connect path completed, so the JS sat forever waiting
on a job.finished that already passed. JobHub split into
Register + Send + Run; handleJobStream now subscribes first,
re-fetches the job, and sends a synthetic job.finished if the
state is already terminal.
* HTMX error visibility. New toast partial listens to
htmx:responseError and surfaces the response body as a
bottom-right toast — every server-side validation error now
becomes visible without per-handler JS wiring. Also handles
custom rm:toast events for future server-pushed notifications
via the HX-Trigger header. Themed via existing CSS vars.
* Dashboard rows are now whole-row clickable to host detail
(CSS card-link pattern: absolute-positioned anchor + .row-action
z-index restoration so the action button stays clickable).
"View →" on a running job links to /jobs/<id> rather than
/hosts/<id> since the row click already covers the host page.
* "Run first" / "Run first backup" → "Run now" everywhere for
consistency.
* runbook (docs/e2e-smoke.md) updated — live-log streaming step
now reflects P1-26; mentions the browser-driven Run-now flow.
* _diag/dump-creds — moved out of cmd/ so go build doesn't pick
it up; .gitignore now excludes /_diag/ entirely.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
148 lines
4.5 KiB
Bash
Executable File
148 lines
4.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# install.sh — Linux installer for the restic-manager agent.
|
|
#
|
|
# Usage (paste in shell):
|
|
# curl -fsSL https://restic.lab.example/install/install.sh | \
|
|
# sudo RM_SERVER=https://restic.lab.example RM_TOKEN=<one-time-token> bash
|
|
#
|
|
# What it does:
|
|
# 1. detects arch (amd64 / arm64)
|
|
# 2. fetches the matching agent binary from the server
|
|
# 3. lays down /etc/restic-manager/, /var/lib/restic-manager/ (root:root, 0700)
|
|
# 4. enrolls (POST /api/agents/enroll) using RM_TOKEN
|
|
# 5. installs the systemd unit, enables, starts
|
|
# 6. surfaces (but does NOT disable) any existing restic timers /
|
|
# cron entries so the operator can decide what to do
|
|
#
|
|
# The agent runs as root. See restic-manager-agent.service for the
|
|
# rationale (in short: a fleet-backup tool must read every file on
|
|
# the system; trying to do that unprivileged buys very little
|
|
# security and creates large UX cliffs).
|
|
#
|
|
# Idempotent — safe to re-run; will refuse if already enrolled
|
|
# unless RM_FORCE_REENROLL=1 is set.
|
|
|
|
set -euo pipefail
|
|
|
|
: "${RM_SERVER:?must be set, e.g. https://restic.lab.example}"
|
|
: "${RM_TOKEN:?must be set, the one-time token from the operator UI}"
|
|
: "${RM_INSTALL_PREFIX:=/usr/local/bin}"
|
|
: "${RM_CONFIG_DIR:=/etc/restic-manager}"
|
|
: "${RM_STATE_DIR:=/var/lib/restic-manager}"
|
|
: "${RM_FORCE_REENROLL:=0}"
|
|
|
|
require_root() {
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
echo "install.sh: must be run as root" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
detect_arch() {
|
|
case "$(uname -m)" in
|
|
x86_64|amd64) echo amd64 ;;
|
|
aarch64|arm64) echo arm64 ;;
|
|
*) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;;
|
|
esac
|
|
}
|
|
|
|
ensure_dirs() {
|
|
install -d -m 0700 -o root -g root "$RM_CONFIG_DIR"
|
|
install -d -m 0700 -o root -g root "$RM_STATE_DIR"
|
|
}
|
|
|
|
detect_existing_schedulers() {
|
|
echo
|
|
echo "==> Scanning for existing restic schedules (we will NOT touch them)"
|
|
local found=0
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
while IFS= read -r unit; do
|
|
[ -n "$unit" ] || continue
|
|
echo " [systemd] $unit"
|
|
echo " disable with: systemctl disable --now $unit"
|
|
found=1
|
|
done < <(systemctl list-unit-files --no-legend --type=timer 2>/dev/null \
|
|
| awk 'tolower($1) ~ /restic/ {print $1}')
|
|
fi
|
|
for f in /etc/cron.d/* /etc/cron.daily/* /etc/cron.hourly/* /etc/cron.weekly/*; do
|
|
[ -f "$f" ] || continue
|
|
if grep -qiI restic "$f" 2>/dev/null; then
|
|
echo " [cron] $f"
|
|
echo " review and remove or rename if you want this agent to take over"
|
|
found=1
|
|
fi
|
|
done
|
|
if root_cron=$(crontab -l 2>/dev/null); then
|
|
if echo "$root_cron" | grep -qi restic; then
|
|
echo " [crontab -l] root crontab contains a restic entry"
|
|
echo " review with: crontab -l"
|
|
found=1
|
|
fi
|
|
fi
|
|
if [ $found -eq 0 ]; then
|
|
echo " (none found)"
|
|
fi
|
|
echo
|
|
}
|
|
|
|
download_agent() {
|
|
local arch out
|
|
arch=$(detect_arch)
|
|
out="$RM_INSTALL_PREFIX/restic-manager-agent"
|
|
|
|
echo "==> Downloading restic-manager-agent (linux/$arch) from $RM_SERVER"
|
|
# The server's /agent/binary endpoint serves the matching binary
|
|
# for the requesting agent's arch. (P1-31; until then this URL
|
|
# may need to be a static download.)
|
|
curl -fsSL --retry 3 \
|
|
"$RM_SERVER/agent/binary?os=linux&arch=$arch" \
|
|
-o "$out.new"
|
|
chmod +x "$out.new"
|
|
mv -f "$out.new" "$out"
|
|
echo " installed $out ($(file -b "$out" | head -1))"
|
|
}
|
|
|
|
enroll_agent() {
|
|
local cfg="$RM_CONFIG_DIR/agent.yaml"
|
|
if [ -s "$cfg" ] && [ "$RM_FORCE_REENROLL" != "1" ]; then
|
|
echo "==> $cfg already exists; skipping enrollment"
|
|
echo " (set RM_FORCE_REENROLL=1 to overwrite)"
|
|
return
|
|
fi
|
|
|
|
echo "==> Enrolling agent with $RM_SERVER"
|
|
"$RM_INSTALL_PREFIX/restic-manager-agent" \
|
|
-config "$cfg" \
|
|
-enroll-server "$RM_SERVER" \
|
|
-enroll-token "$RM_TOKEN"
|
|
}
|
|
|
|
install_unit() {
|
|
local unit="/etc/systemd/system/restic-manager-agent.service"
|
|
echo "==> Installing systemd unit at $unit"
|
|
curl -fsSL --retry 3 \
|
|
"$RM_SERVER/install/restic-manager-agent.service" \
|
|
-o "$unit"
|
|
chmod 0644 "$unit"
|
|
systemctl daemon-reload
|
|
systemctl enable --now restic-manager-agent.service
|
|
echo " started; tail with: journalctl -fu restic-manager-agent"
|
|
}
|
|
|
|
main() {
|
|
require_root
|
|
ensure_dirs
|
|
download_agent
|
|
detect_existing_schedulers
|
|
enroll_agent
|
|
install_unit
|
|
echo
|
|
echo "==> done."
|
|
echo " config: $RM_CONFIG_DIR/agent.yaml"
|
|
echo " binary: $RM_INSTALL_PREFIX/restic-manager-agent"
|
|
echo " service: systemctl status restic-manager-agent"
|
|
echo " logs: journalctl -fu restic-manager-agent"
|
|
}
|
|
|
|
main "$@"
|