f0dfa689fe
Three small follow-ups from review:
1. Restore target is now operator-editable. Default value is the
literal '\$HOME/rm-restore/<job-id>/' (agent expands \$HOME at
run time using os.UserHomeDir(); also handles \${HOME} and ~/
prefixes). Operator can replace with any absolute path.
- ui_restore.go validates the input is either absolute or starts
with one of the recognised prefixes; other env-var refs (\$PATH
etc.) are deliberately rejected so operator paths can't pick up
arbitrary agent env values.
- host_restore.html replaces the read-only mono-text display with
a real <input>; help text spells out that \$HOME resolves
agent-side and <job-id> is substituted on dispatch.
- install.sh + the systemd unit prep /root/rm-restore so the
default works under the sandbox: ReadWritePaths gains a soft
'-/root/rm-restore' entry (the '-' makes the bind-mount soft-fail
if missing, but install.sh pre-creates it root-owned 0700).
2. --no-ownership flag now gated on restic version. The flag was
added in restic 0.17 and 0.16 rejects it. Previously dropped it
wholesale — that meant new-dir restores silently preserved
ownership against design intent on 0.17+. Now the agent threads
its detected restic version (sysinfo already collects it) through
runner.Config -> restic.Env, and RunRestore appends --no-ownership
only when AtLeastVersion(0, 17) returns true. 0.16 hosts still
restore with original uid/gid; help text in the wizard explicitly
notes this. The previous 'Original ownership is preserved' copy
was wrong for new-dir mode and is corrected.
3. golangci-lint misspell locale switched US -> UK and the codebase
swept (73 corrections, mostly behaviour/serialise/recognise/honour).
Wire-format ErrorCode 'unauthorized' -> 'unauthorised' is a tiny
contract change but the agent doesn't parse those codes today and
no external API consumers exist yet. Tests passed before + after.
Tests:
- internal/restic/version_test.go covers Env.AtLeastVersion across
edge cases (empty, exact match, patch above, minor below, non-
numeric) and expandHome on \$HOME / \${HOME} / ~/, plus
pass-through for absolute paths and refusal of other env vars.
- ui_restore_test updated: TargetDir now starts '\$HOME/rm-restore/'
with the job_id substituted into the placeholder.
Live verified on the smoke env: default target restored to
/root/rm-restore/<job-id>/ as the agent's expanded \$HOME (2 files,
14 bytes); custom override '/tmp/custom-restore/<job-id>/' restored
into the agent's PrivateTmp namespace (1 file, 6 bytes); both jobs
'succeeded', exit 0.
155 lines
5.0 KiB
Bash
Executable File
155 lines
5.0 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"
|
|
# Default new-directory restore target: $HOME/rm-restore. Pre-create
|
|
# so the systemd unit's ReadWritePaths bind-mount applies cleanly
|
|
# (paths that don't exist when systemd starts get a soft-fail
|
|
# because of the '-' prefix, but the agent then can't mkdir into
|
|
# the read-only /root). Mode 0700 + root-owned matches the threat
|
|
# model — files restored here are operator-readable as root.
|
|
install -d -m 0700 -o root -g root /root/rm-restore
|
|
}
|
|
|
|
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 "$@"
|