Add Servers & Tasks module ported from Schedule Task Manager
Ports cron/systemd task tracking across Debian/Raspbian servers, including the Linux push agent (install/report/uninstall scripts, rebranded from "schedule-task-manager-agent" to "homelab-manager-agent") and the manual-task-entry flow for things an agent can't see (Docker jobs, backups). The schema (servers/scheduled_tasks tables) was already in place from the foundation pass, so this is mostly a straight port of the original's services/routes. Deliberate change from the original: server/token management (which mints agent credentials) is now admin-only rather than open to any logged-in user, and manual task CRUD is gated to operator+ — consistent with how Secrets, IPAM, and DNS already split "configure credentials" from "everyday edits" across roles. All mutations are audit-logged. - server/src/services/tokens.ts, taskSync.ts: ported near-verbatim (agent token hashing, the agent-sync-marks-missing-as-stale-not-deleted logic). - server/src/routes/servers.ts, tasks.ts, agentReport.ts: same contract as the original (agent auth is a per-server bearer token, independent of the session-based requireAuth used everywhere else). - web: a single Servers & Tasks page (filter bar, task table grouped by server/schedule type, manual task form, and an admin-only server management panel with token reveal + copyable install/uninstall commands), replacing the original's two separate pages/apps. Verified: full build passes; a scripted HTTP test against a running server covers unauthenticated access, role gating at each tier (admin-only server mgmt, operator+ task mgmt), agent bearer-token auth (valid/invalid/rotated), manual-vs-agent task edit protection, stale-marking on re-sync, and cascade delete — 22/22 checks passing. Real agent installation on an actual Debian/Raspbian host still needs to be tried on the user's network. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ac3feb935d
commit
9712d611a6
17 files changed
+1616
-1
No files matched your search
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installs the Homelab Manager task-reporting agent as a systemd timer.
|
||||
#
|
||||
# Usage (must run as root — if not already root, put sudo right after the
|
||||
# pipe so it applies to bash, not to curl):
|
||||
# curl -fsSL https://homelab.example.lan/agent/linux/install.sh | \
|
||||
# sudo API_URL=https://homelab.example.lan API_TOKEN=hlm_xxx bash
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
: "${API_URL:?Set API_URL to your Homelab Manager URL, e.g. https://homelab.example.lan}"
|
||||
: "${API_TOKEN:?Set API_TOKEN to the per-server token generated on the Servers & Tasks page}"
|
||||
|
||||
INSTALL_DIR="/usr/local/bin"
|
||||
CONFIG_DIR="/etc"
|
||||
SYSTEMD_DIR="/etc/systemd/system"
|
||||
INTERVAL_MINUTES="${INTERVAL_MINUTES:-15}"
|
||||
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
echo "This installer must be run as root (it installs a systemd timer)." >&2
|
||||
echo "If you're piping from curl, put sudo right after the pipe so it elevates bash, not curl:" >&2
|
||||
echo " curl -fsSL ... | sudo API_URL=... API_TOKEN=... bash" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
suggest_package_install() {
|
||||
local pkg="$1"
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
echo " sudo apt-get update && sudo apt-get install -y $pkg"
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
echo " sudo dnf install -y $pkg"
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
echo " sudo yum install -y $pkg"
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
echo " sudo apk add $pkg"
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
echo " sudo pacman -S $pkg"
|
||||
elif command -v zypper >/dev/null 2>&1; then
|
||||
echo " sudo zypper install -y $pkg"
|
||||
fi
|
||||
}
|
||||
|
||||
for bin in curl jq; do
|
||||
if ! command -v "$bin" >/dev/null 2>&1; then
|
||||
echo "Required dependency '$bin' is not installed. Try:" >&2
|
||||
suggest_package_install "$bin" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if ! command -v systemctl >/dev/null 2>&1; then
|
||||
echo "systemctl was not found — this installer requires a systemd-based Linux distribution." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing Homelab Manager agent from $API_URL ..."
|
||||
|
||||
curl -fsSL "$API_URL/agent/linux/report-tasks.sh" -o "$INSTALL_DIR/homelab-manager-agent.sh"
|
||||
chmod 755 "$INSTALL_DIR/homelab-manager-agent.sh"
|
||||
|
||||
umask 077
|
||||
cat > "$CONFIG_DIR/homelab-manager-agent.env" <<EOF
|
||||
API_URL=$API_URL
|
||||
API_TOKEN=$API_TOKEN
|
||||
EOF
|
||||
chmod 600 "$CONFIG_DIR/homelab-manager-agent.env"
|
||||
|
||||
cat > "$SYSTEMD_DIR/homelab-manager-agent.service" <<EOF
|
||||
[Unit]
|
||||
Description=Report scheduled tasks to Homelab Manager
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
EnvironmentFile=$CONFIG_DIR/homelab-manager-agent.env
|
||||
ExecStart=$INSTALL_DIR/homelab-manager-agent.sh
|
||||
EOF
|
||||
|
||||
cat > "$SYSTEMD_DIR/homelab-manager-agent.timer" <<EOF
|
||||
[Unit]
|
||||
Description=Periodically report scheduled tasks to Homelab Manager
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=${INTERVAL_MINUTES}min
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now homelab-manager-agent.timer
|
||||
|
||||
echo "Installed. Running an initial report now..."
|
||||
"$INSTALL_DIR/homelab-manager-agent.sh"
|
||||
|
||||
echo "Done. The agent reports every ${INTERVAL_MINUTES} minute(s) via the 'homelab-manager-agent.timer' systemd timer."
|
||||
echo "To remove it later: curl -fsSL $API_URL/agent/linux/uninstall.sh | sudo bash"
|
||||
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env bash
|
||||
# Collects cron jobs and systemd timers on this host and POSTs them to the
|
||||
# Homelab Manager API. Intended to run as root via a periodic systemd timer
|
||||
# (see install.sh) but can be run manually for testing:
|
||||
#
|
||||
# API_URL=https://homelab.example.lan API_TOKEN=hlm_xxx ./report-tasks.sh --dry-run
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ENV_FILE="${ENV_FILE:-/etc/homelab-manager-agent.env}"
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
fi
|
||||
|
||||
API_URL="${API_URL:-}"
|
||||
API_TOKEN="${API_TOKEN:-}"
|
||||
DRY_RUN=0
|
||||
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=1
|
||||
|
||||
if [[ -z "$API_URL" || -z "$API_TOKEN" ]]; then
|
||||
echo "API_URL and API_TOKEN must be set (env vars or $ENV_FILE)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
suggest_package_install() {
|
||||
local pkg="$1"
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
echo " sudo apt-get update && sudo apt-get install -y $pkg"
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
echo " sudo dnf install -y $pkg"
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
echo " sudo yum install -y $pkg"
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
echo " sudo apk add $pkg"
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
echo " sudo pacman -S $pkg"
|
||||
elif command -v zypper >/dev/null 2>&1; then
|
||||
echo " sudo zypper install -y $pkg"
|
||||
fi
|
||||
}
|
||||
|
||||
for bin in curl jq; do
|
||||
if ! command -v "$bin" >/dev/null 2>&1; then
|
||||
echo "Required dependency '$bin' is not installed. Try:" >&2
|
||||
suggest_package_install "$bin" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
TASKS_JSON="[]"
|
||||
|
||||
add_task() {
|
||||
local schedule_type="$1" name="$2" command="$3" schedule_expression="$4" source="$5" enabled="$6"
|
||||
TASKS_JSON=$(jq -c \
|
||||
--arg schedule_type "$schedule_type" \
|
||||
--arg name "$name" \
|
||||
--arg command "$command" \
|
||||
--arg schedule_expression "$schedule_expression" \
|
||||
--arg source "$source" \
|
||||
--argjson enabled "$enabled" \
|
||||
'. + [{
|
||||
schedule_type: $schedule_type,
|
||||
name: $name,
|
||||
command: $command,
|
||||
schedule_expression: $schedule_expression,
|
||||
source: $source,
|
||||
enabled: $enabled
|
||||
}]' <<<"$TASKS_JSON")
|
||||
}
|
||||
|
||||
add_task_with_next_run() {
|
||||
local schedule_type="$1" name="$2" command="$3" schedule_expression="$4" source="$5" enabled="$6" next_run_at="$7"
|
||||
TASKS_JSON=$(jq -c \
|
||||
--arg schedule_type "$schedule_type" \
|
||||
--arg name "$name" \
|
||||
--arg command "$command" \
|
||||
--arg schedule_expression "$schedule_expression" \
|
||||
--arg source "$source" \
|
||||
--argjson enabled "$enabled" \
|
||||
--arg next_run_at "$next_run_at" \
|
||||
'. + [{
|
||||
schedule_type: $schedule_type,
|
||||
name: $name,
|
||||
command: $command,
|
||||
schedule_expression: $schedule_expression,
|
||||
source: $source,
|
||||
enabled: $enabled,
|
||||
next_run_at: $next_run_at
|
||||
}]' <<<"$TASKS_JSON")
|
||||
}
|
||||
|
||||
parse_crontab_lines() {
|
||||
# Reads 5-field-schedule + command cron lines from stdin.
|
||||
# $1 = source label, $2 = "system" (7-field, includes a user column) or "user" (6-field)
|
||||
local source="$1" mode="$2"
|
||||
while IFS= read -r line; do
|
||||
line="${line%%$'\r'}"
|
||||
[[ -z "$line" ]] && continue
|
||||
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||
[[ "$line" =~ ^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*= ]] && continue
|
||||
|
||||
if [[ "$mode" == "system" ]]; then
|
||||
# min hour dom mon dow user command...
|
||||
if [[ "$line" =~ ^[[:space:]]*([^[:space:]]+)[[:space:]]+([^[:space:]]+)[[:space:]]+([^[:space:]]+)[[:space:]]+([^[:space:]]+)[[:space:]]+([^[:space:]]+)[[:space:]]+[^[:space:]]+[[:space:]]+(.+)$ ]]; then
|
||||
local sched="${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]} ${BASH_REMATCH[4]} ${BASH_REMATCH[5]}"
|
||||
local cmd="${BASH_REMATCH[6]}"
|
||||
add_task "cron" "$cmd" "$cmd" "$sched" "$source" true
|
||||
fi
|
||||
else
|
||||
# min hour dom mon dow command...
|
||||
if [[ "$line" =~ ^[[:space:]]*([^[:space:]]+)[[:space:]]+([^[:space:]]+)[[:space:]]+([^[:space:]]+)[[:space:]]+([^[:space:]]+)[[:space:]]+([^[:space:]]+)[[:space:]]+(.+)$ ]]; then
|
||||
local sched="${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]} ${BASH_REMATCH[4]} ${BASH_REMATCH[5]}"
|
||||
local cmd="${BASH_REMATCH[6]}"
|
||||
add_task "cron" "$cmd" "$cmd" "$sched" "$source" true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
collect_cron() {
|
||||
[[ -r /etc/crontab ]] && parse_crontab_lines "/etc/crontab" "system" < /etc/crontab
|
||||
|
||||
if [[ -d /etc/cron.d ]]; then
|
||||
for f in /etc/cron.d/*; do
|
||||
[[ -f "$f" && -r "$f" ]] || continue
|
||||
parse_crontab_lines "$f" "system" < "$f"
|
||||
done
|
||||
fi
|
||||
|
||||
if command -v crontab >/dev/null 2>&1 && [[ -r /etc/passwd ]]; then
|
||||
while IFS=: read -r user _ uid _ _ _ shell; do
|
||||
case "$shell" in
|
||||
*/nologin|*/false|"") continue ;;
|
||||
esac
|
||||
[[ "$uid" -lt 1000 && "$uid" != "0" ]] && continue
|
||||
local_crontab=$(crontab -l -u "$user" 2>/dev/null || true)
|
||||
[[ -z "$local_crontab" ]] && continue
|
||||
parse_crontab_lines "crontab:$user" "user" <<<"$local_crontab"
|
||||
done < /etc/passwd
|
||||
fi
|
||||
}
|
||||
|
||||
collect_systemd_timers() {
|
||||
command -v systemctl >/dev/null 2>&1 || return 0
|
||||
|
||||
local timers_json
|
||||
timers_json=$(systemctl list-timers --all --output=json 2>/dev/null || echo "[]")
|
||||
|
||||
while IFS= read -r entry; do
|
||||
local unit activates next_usec enabled_state schedule_expr next_run_at
|
||||
unit=$(jq -r '.unit' <<<"$entry")
|
||||
activates=$(jq -r '.activates // ""' <<<"$entry")
|
||||
next_usec=$(jq -r '.next // 0' <<<"$entry")
|
||||
|
||||
enabled_state=$(systemctl is-enabled "$unit" 2>/dev/null || echo "unknown")
|
||||
local enabled_bool="false"
|
||||
[[ "$enabled_state" == "enabled" || "$enabled_state" == "static" ]] && enabled_bool="true"
|
||||
|
||||
schedule_expr=$(systemctl show "$unit" -p TimersCalendar --value 2>/dev/null | sed -n 's/.*OnCalendar=\([^;]*\);.*/\1/p' | sed 's/[[:space:]]*$//')
|
||||
[[ -z "$schedule_expr" ]] && schedule_expr="(see: systemctl status $unit)"
|
||||
|
||||
next_run_at=""
|
||||
if [[ "$next_usec" =~ ^[0-9]+$ && "$next_usec" -gt 0 ]]; then
|
||||
next_run_at=$(date -u -d "@$((next_usec / 1000000))" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [[ -n "$next_run_at" ]]; then
|
||||
add_task_with_next_run "systemd_timer" "$unit" "$activates" "$schedule_expr" "$unit" "$enabled_bool" "$next_run_at"
|
||||
else
|
||||
add_task "systemd_timer" "$unit" "$activates" "$schedule_expr" "$unit" "$enabled_bool"
|
||||
fi
|
||||
done < <(jq -c '.[]' <<<"$timers_json")
|
||||
}
|
||||
|
||||
collect_cron
|
||||
collect_systemd_timers
|
||||
|
||||
HOSTNAME_VALUE=$(hostname -f 2>/dev/null || hostname)
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg hostname "$HOSTNAME_VALUE" \
|
||||
--arg os_type "linux" \
|
||||
--arg reported_at "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
|
||||
--argjson tasks "$TASKS_JSON" \
|
||||
'{hostname: $hostname, os_type: $os_type, reported_at: $reported_at, tasks: $tasks}')
|
||||
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
echo "$PAYLOAD" | jq .
|
||||
exit 0
|
||||
fi
|
||||
|
||||
response=$(curl -sS -o /tmp/hlm-agent-response.json -w "%{http_code}" \
|
||||
-X POST "$API_URL/api/agent/report" \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD")
|
||||
|
||||
if [[ "$response" -lt 200 || "$response" -ge 300 ]]; then
|
||||
echo "Report failed with HTTP $response:" >&2
|
||||
cat /tmp/hlm-agent-response.json >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Reported $(jq 'length' <<<"$TASKS_JSON") task(s) successfully."
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# Removes the Homelab Manager task-reporting agent from this server: stops
|
||||
# and deletes its systemd timer/service, the installed script, and its
|
||||
# credentials file.
|
||||
#
|
||||
# Usage (must run as root — if not already root, put sudo right after the
|
||||
# pipe so it applies to bash, not to curl):
|
||||
# curl -fsSL https://homelab.example.lan/agent/linux/uninstall.sh | sudo bash
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_DIR="/usr/local/bin"
|
||||
CONFIG_DIR="/etc"
|
||||
SYSTEMD_DIR="/etc/systemd/system"
|
||||
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
echo "This uninstaller must be run as root." >&2
|
||||
echo "If you're piping from curl, put sudo right after the pipe so it elevates bash, not curl:" >&2
|
||||
echo " curl -fsSL ... | sudo bash" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Removing Homelab Manager agent..."
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl disable --now homelab-manager-agent.timer >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
rm -f \
|
||||
"$SYSTEMD_DIR/homelab-manager-agent.timer" \
|
||||
"$SYSTEMD_DIR/homelab-manager-agent.service" \
|
||||
"$CONFIG_DIR/homelab-manager-agent.env" \
|
||||
"$INSTALL_DIR/homelab-manager-agent.sh" \
|
||||
/tmp/hlm-agent-response.json
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
|
||||
echo "Done. The agent no longer runs or reports from this server."
|
||||
echo "Its entry (and task history) in Homelab Manager is untouched —" \
|
||||
"delete it from the Servers & Tasks page if you no longer want it tracked."
|
||||
@@ -0,0 +1,14 @@
|
||||
# Windows agent (planned, not yet implemented)
|
||||
|
||||
v1 of the Servers & Tasks module only supports Linux servers (cron + systemd
|
||||
timers) — this covers the Debian and Raspbian hosts in the homelab. A Windows
|
||||
agent is a natural future addition and would follow the same contract as the
|
||||
Linux agent in [`../linux/report-tasks.sh`](../linux/report-tasks.sh):
|
||||
|
||||
- Collect tasks with `Get-ScheduledTask | Get-ScheduledTaskInfo` (name, action/command,
|
||||
trigger description, next run time, enabled state).
|
||||
- POST the same JSON shape to `POST /api/agent/report` with `schedule_type: "windows_task"`
|
||||
(the server and UI already treat `schedule_type` as an open string in storage; only the
|
||||
`tasks` API and UI schedule-type filter would need the new value added).
|
||||
- Ship as a scheduled task (naturally) or a small Windows service that runs on a timer,
|
||||
configured via the same `API_URL` / `API_TOKEN` environment variables as the Linux agent.
|
||||
Reference in new issue
Block a user