install.sh (#14): after enrolling it now BLOCKS until an admin approves, printing "Please have an admin approve node '<id>' at <app>/settings/nodes to continue", polling GET /nodes/{id}/credentials with the pickup secret (no default timeout; Ctrl-C safe; re-run resumes from configs/pickup_secret). A re-run with a key on disk re-checks it against C2 and replaces a stale (reissued) key. credentials.json is merged atomically so the dashboard auth salt/secret survive. edge-node is ALWAYS force-recreated at the end, so a key written after the container started is loaded. --wait-approval N caps the wait; 0 = don't wait. git fetch uses --force so a re-cut v1 tag is accepted. edge-node (#14): with no key at startup, or on a broker auth refusal (CONNACK 4/5/134/135), mqtt_manager re-reads credentials.json, else fetches the key over HTTPS (key_pickup.py, pickup secret from /configs), persists it and rebuilds the paho client in place. Single-flight, 30s..15min backoff. A key pushed over MQTT while connected is applied for the next reconnect. OTA (#15): node_update no longer os._exit()s. It drops configs/update-requested; install.sh installs scripts/install-updater.sh's drb-update.{service,timer,path}. The path unit fires on the sentinel, the timer (~15 min) does an HTTPS check-in (POST /nodes/{id}/update-check, server-26 PR) so offline/locked-out nodes still update. drb-update.sh only installs the repo/ref/registry in root-owned /etc/drb/updater.conf; C2's answer is just a timestamp; origin and registry mismatches abort; creds go to curl via a 0600 header file. Tests: test_credential_recovery.py, test_host_updater.py (runs the real updater script with stub git/docker/curl). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6bf2c9dd93
commit
c2c349043b
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Hand an MQTT `node_update` command to the host-level updater (node-26#15).
|
||||
|
||||
The edge-node container has no business updating its own host: it writes a
|
||||
sentinel file into /configs (bind-mounted from <install dir>/configs), and the
|
||||
host's systemd drb-update.path unit — installed by install.sh — triggers
|
||||
drb-update.service, which runs Client/scripts/drb-update.sh. The host treats
|
||||
the file as a bare "please update" flag and never reads its content as input,
|
||||
so nothing C2 sends can pick the ref, image or command that runs.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from app.config import settings
|
||||
from app.internal.logger import logger
|
||||
|
||||
SENTINEL_NAME = "update-requested"
|
||||
|
||||
|
||||
def sentinel_path() -> Path:
|
||||
return Path(settings.config_path) / SENTINEL_NAME
|
||||
|
||||
|
||||
def request_host_update() -> bool:
|
||||
"""Drop the sentinel. Returns False if it could not be written."""
|
||||
path = sentinel_path()
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = path.with_name(SENTINEL_NAME + ".tmp")
|
||||
tmp.write_text(json.dumps({"requested_at": datetime.now(timezone.utc).isoformat()}))
|
||||
os.replace(tmp, path)
|
||||
except OSError as e:
|
||||
logger.error(f"Node update requested but the sentinel could not be written ({e}).")
|
||||
return False
|
||||
logger.info(
|
||||
f"Node update requested — handed to the host updater via {path}. If nothing happens within a minute, "
|
||||
"this node predates the updater: re-run install.sh on it once."
|
||||
)
|
||||
return True
|
||||
Reference in New Issue
Block a user