phase: 33_gateway_route_switching
Phase 33 completion pass is done. Final report:
## Phase 33 — completion report
**All 8 task files complete** (`.agents/phases/complete/33_gateway_route_switching/`), full gate green, docs verified.
### Work in this pass
1. **`tests/test_routectl.py` added** (17 tests, all green) — exercises the *real* helper binary unprivileged against a fake tree (`--root`), the real agent parser, the captured gateway fixtures, and stubbed `nginx`/`haproxy`/`systemctl` on `PATH`. Pins the full protocol (locate → state → dry-run → temp-file/fsync/`os.replace` write → checker → restore on checker failure → reload-never-restart → re-read proof → sidecar record), all three route shapes (haproxy `server`, nginx `proxy_pass`, nginx stream `map` via its sink-value id), `drift` / unknown-id / `map-default` refusals, `list`/`verify`, and the `--serve` peer gate (`SO_PEERCRED` uid check, one JSON line per request, exact body shape).
2. **Real helper defect fixed** — `deploy/dashboards-routectl.py`: `main()`'s early-return paths returned `({"ok": False, …}, EXIT_*)` tuples, so `sys.exit()` printed a Python repr instead of a JSON line (usage errors, agent-import refusal). Now prints `json.dumps(…)` and returns only the code, keeping the one-JSON-line contract.
3. **Documentation consistency fixes** (task 08 criterion 6):
- `README.md`: garbled verdict-colour sentence on the switch ("green and to the position") → "in the **on** position and tinted `--gate-on`" (the existence-pair contract).
- `README.md`: audit sentence now names all three event kinds — `route-disabled` / `route-enabled` / **`route-denied`** (the code stores one per refused call; dry runs store none).
- `page-gateway.js`: replaced the stale pre-33 module docstring ("the switch is not wired") with the two-arm `GATEWAY_ALLOW_WRITES` contract.
- `AGENTS.md`: every phase-33 claim re-verified against code (the ten gates, the 35 s/30 s budget pair, `gateway.writes_enabled`, `/api/summary`'s `disabled`, the 1.4.0 agent bullet, the gap list) — no changes needed.
### Verification
- `uv run pytest --cov`: **1970 passed**, 18 skipped — coverage **98.74%** (floor 94%)
- `ruff` clean · `pyright` 0 errors · `bash .agents/validate.sh` **exit 0** (incl. `node --check` on every page script)
- Dev server running; all 10 sources `ok`; bundles exported
### Live infrastructure
**No live changes made.** The live drill remains unexecuted per `data/logs/NOTES-33-switching.md` (no operator approval; checker binaries unavailable in the podman proxy units). All routes stay enabled, config hashes unchanged, and the sandbox rehearsal + backup tree are in place for the operator's own `DANGER` call. All changes are in the working tree only.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
# dashboards-routectl.socket
|
||||
#
|
||||
# The socket the agent (and nothing else) may talk to for a route switch.
|
||||
#
|
||||
# 0600 + SocketUser=dashboards-agent is the whole access control at the file
|
||||
# layer: only that uid can connect; everyone else gets EACCES before any code
|
||||
# of ours runs. The helper's SO_PEERCRED check (its --allow-uid) is the
|
||||
# *second* look, not the first.
|
||||
#
|
||||
# One connection, one instance: `Accept=yes` is what makes it so — the socket
|
||||
# unit accepts the connection itself and hands the *connection* (not the
|
||||
# listener) to a fresh `dashboards-routectl@<instance>` on stdin (StandardInput=socket);
|
||||
# the instance answers one request and exits. There is no long-lived privileged
|
||||
# process. (A `Service=` line naming the template does not load on systemd 257 —
|
||||
# "missing the instance name" — `Accept=yes` is the documented template path.)
|
||||
[Socket]
|
||||
Accept=yes
|
||||
ListenStream=/run/dashboards-routectl.sock
|
||||
SocketUser=dashboards-agent
|
||||
SocketGroup=dashboards-agent
|
||||
SocketMode=0600
|
||||
NoDelay=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
@@ -0,0 +1,42 @@
|
||||
# dashboards-routectl@.service
|
||||
#
|
||||
# The route-switch helper, root, socket-activated, one request per instance.
|
||||
#
|
||||
# The privilege model, in one line: the grant is a file mode systemd enforces
|
||||
# (the socket is 0600 and owned by the agent user), not a sudoers line. This
|
||||
# unit is how the agent's one narrow write reaches root without the agent's
|
||||
# own unit being given anything — NoNewPrivileges=true is kept *here* as well,
|
||||
# because the privilege comes from being started by systemd as root, not from
|
||||
# escalation.
|
||||
#
|
||||
# Write scope, stated out loud: the helper replaces exactly two kinds of
|
||||
# config files, and os.replace needs write on the *directory*, so
|
||||
# ReadWritePaths lists every directory whose file it replaces — and nothing
|
||||
# else:
|
||||
# /etc/haproxy the haproxy.cfg `server` line (the haproxy arm)
|
||||
# /etc/nginx/conf.d the vhost `proxy_pass` line (the vhost arm)
|
||||
# /etc/nginx/stream.d the `map` entry (the stream arm) and the helper's
|
||||
# own sink file, 00-dashboards-sink.conf, which it
|
||||
# creates and removes there
|
||||
# /etc/nginx (the main config) is deliberately not listed: the helper never
|
||||
# writes it.
|
||||
#
|
||||
# One connection, one instance: the socket's `Accept=yes` hands this template a
|
||||
# fresh connected socket on stdin per connection (StandardInput=socket /
|
||||
# StandardOutput=socket); the helper answers exactly one newline-delimited JSON
|
||||
# request and exits, so there is never a long-lived privileged process sitting
|
||||
# on a socket.
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/sbin/dashboards-routectl.py --serve \
|
||||
--allow-uid dashboards-agent --state /var/lib/dashboards-routectl/state.json
|
||||
User=root
|
||||
StandardInput=socket
|
||||
StandardOutput=socket
|
||||
StateDirectory=dashboards-routectl
|
||||
ProtectSystem=read-only
|
||||
ReadWritePaths=/etc/haproxy /etc/nginx/conf.d /etc/nginx/stream.d
|
||||
ProtectHome=read-only
|
||||
PrivateTmp=yes
|
||||
NoNewPrivileges=true
|
||||
SystemCallFilter=@system-service
|
||||
@@ -0,0 +1,66 @@
|
||||
# The route-switch helper (gateway hosts)
|
||||
|
||||
The dashboard's one narrow write is a route switch: one named connection on the
|
||||
ingress goes `#off` (a marker the agent's own parser reads) or comes back. It is
|
||||
not a sudoers line. The grant is a **file mode systemd enforces**: the socket the
|
||||
agent talks to is `0600` and owned by the agent's own user, and the helper's
|
||||
peer check (`--allow-uid`) is the second look. No account on the host — `ops`
|
||||
included — can reach the write; only the agent's uid can connect, and only that
|
||||
uid is accepted on the other end.
|
||||
|
||||
## The two units
|
||||
|
||||
| unit | what it is |
|
||||
| ---- | ---------- |
|
||||
| `dashboards-routectl.socket` | a systemd-activated stream socket at `/run/dashboards-routectl.sock`, `0600`, owned by `dashboards-agent` |
|
||||
| `dashboards-routectl@.service` | the helper, **root**, one instance per connection (the connected socket on stdin), answers one request and exits |
|
||||
|
||||
The service unit's `ReadWritePaths` lists exactly the directories whose file the
|
||||
helper replaces — `/etc/haproxy`, `/etc/nginx/conf.d`, `/etc/nginx/stream.d` —
|
||||
and nothing else. There is never a long-lived privileged process: each request
|
||||
spawns a fresh instance that exits after its one answer.
|
||||
|
||||
## See them
|
||||
|
||||
```
|
||||
systemctl status dashboards-routectl.socket
|
||||
systemctl status 'dashboards-routectl@*.service'
|
||||
journalctl -u 'dashboards-routectl@*'
|
||||
```
|
||||
|
||||
## Run the helper by hand on the box
|
||||
|
||||
The installed copy is `/usr/local/sbin/dashboards-routectl.py`. It takes its
|
||||
agent parser by file path and refuses to run without one:
|
||||
|
||||
```
|
||||
# what is off (the state sidecar)
|
||||
sudo /usr/local/sbin/dashboards-routectl.py list
|
||||
|
||||
# preview one route without writing a byte (no checker, no reload, no state)
|
||||
sudo /usr/local/sbin/dashboards-routectl.py diff --id nginx:443->127.0.0.1:44450 --action enable
|
||||
|
||||
# verify the sidecar against the files (drift is reported, never repaired)
|
||||
sudo /usr/local/sbin/dashboards-routectl.py verify
|
||||
```
|
||||
|
||||
A manual `disable --apply` runs the whole protocol: write, the proxy's own
|
||||
checker (`nginx -t` / `haproxy -c -f`), restore the previous bytes on a checker
|
||||
failure, `systemctl reload` (never `restart`) on success, then record the state.
|
||||
|
||||
## Undo it all
|
||||
|
||||
```
|
||||
sudo ./deploy/install-agent.sh --without-routectl
|
||||
```
|
||||
|
||||
That disables and removes the helper and its two units, **leaves the proxy
|
||||
configs and the helper's state directory alone**, and prints the list of
|
||||
still-off routes first — those are yours to switch back on.
|
||||
|
||||
## The one safety sentence
|
||||
|
||||
No sudoers line is involved. The agent's unit keeps every one of its security
|
||||
directives (`NoNewPrivileges=true`, `ProtectSystem=strict`, …); the read-only
|
||||
collector stayed read-only in its sandbox and talked to a peer outside it. The
|
||||
grant is the socket's mode and ownership, enforced by systemd.
|
||||
+154
-2
@@ -9,8 +9,11 @@
|
||||
# # registration fetches its own bundle
|
||||
# sudo ./deploy/install-agent.sh --as-root --caps CAP_SYS_RAWIO,CAP_SYS_ADMIN
|
||||
# # a kernel-gated host (see below)
|
||||
# sudo ./deploy/install-agent.sh --with-routectl # also install the route-switch helper
|
||||
# # and its two units (the gateway only)
|
||||
# ./deploy/install-agent.sh --status # unit state + /health, no root
|
||||
# sudo ./deploy/install-agent.sh --uninstall
|
||||
# sudo ./deploy/install-agent.sh --without-routectl # remove helper + units, keep config and state
|
||||
# sudo ./deploy/install-agent.sh --force # re-apply over a unit this would change
|
||||
#
|
||||
# No root shell? `deploy/agent-sudoers` is the single sudoers line that lets the fleet's
|
||||
@@ -102,6 +105,17 @@
|
||||
# sudo ./deploy/install-agent.sh --bind <addr> --port 9120 \
|
||||
# --as-root --caps CAP_SYS_RAWIO,CAP_SYS_ADMIN
|
||||
#
|
||||
# --with-routectl (gateway hosts only) installs the one narrow write the dashboard can make:
|
||||
# the root route-switch helper as a socket-activated service. The grant is a file mode systemd
|
||||
# enforces — the socket is 0600 and owned by the agent's own user, and the helper's peer check
|
||||
# (--allow-uid) is the second look — not a sudoers line, and the agent's unit is not changed by
|
||||
# it: an install with the flag renders the agent unit byte-identically to one without it. The
|
||||
# helper edits exactly one route (a #off <name> marker the agent's parser reads), validates the
|
||||
# file with the proxy's own checker before it means anything, restores the previous bytes on a
|
||||
# checker failure, and reloads — never restarts. --without-routectl removes the helper and its
|
||||
# two units, leaves the proxy configs and the helper's state directory alone, and prints the
|
||||
# list of still-off routes first, because those are the operator's to switch back on.
|
||||
#
|
||||
# A re-run is the zero-noise path it has always been as long as the unit this script would
|
||||
# write is the unit already on disk. When the two differ the run stops: the diff between the
|
||||
# installed unit and the generated one is printed and the exit code is 1, before the unit is
|
||||
@@ -113,6 +127,13 @@
|
||||
#
|
||||
# --force overwrite an existing unit even if the generated unit differs (prints the
|
||||
# diff first)
|
||||
#
|
||||
# --allow-writes (gateway hosts) adds the agent's own --allow-writes to the unit's
|
||||
# ExecStart — the agent-side half of the write path; --with-routectl installs the
|
||||
# helper it dials, and the two are independent (a host with the flag but without the
|
||||
# helper answers a write with 503, not with a write). It is the *only* change this
|
||||
# script makes to the agent's unit for the write path: a re-run diff that shows the
|
||||
# security directives moving is a stop, not a --force.
|
||||
set -euo pipefail
|
||||
|
||||
SERVICE="dashboards-agent"
|
||||
@@ -133,6 +154,20 @@ if command -v realpath >/dev/null 2>&1; then
|
||||
SCRIPT_PATH="$(realpath "$0" 2>/dev/null || printf '%s' "$SCRIPT_PATH")"
|
||||
fi
|
||||
SUDOERS_SOURCE="$(dirname "$SCRIPT_PATH")/agent-sudoers"
|
||||
# The route-switch helper (phase 33) and its two units. The helper is the only writer this
|
||||
# phase adds: a root, socket-activated service the agent may talk to and nothing else. The
|
||||
# grant is a file mode systemd enforces (the socket is 0600, owned by the agent's user), not
|
||||
# a sudoers line, and the agent's own unit is not touched by installing it — the rendered
|
||||
# unit is byte-identical with and without --with-routectl, and the re-run guard below applies
|
||||
# to the helper and both units exactly as it does to the agent's.
|
||||
ROUTECTL_SOURCE="$SCRIPT_DIR/dashboards-routectl.py"
|
||||
ROUTECTL_SOCKET_UNIT_SRC="$SCRIPT_DIR/dashboards-routectl.socket"
|
||||
ROUTECTL_SERVICE_UNIT_SRC="$SCRIPT_DIR/dashboards-routectl@.service"
|
||||
ROUTECTL_TARGET="/usr/local/sbin/dashboards-routectl.py"
|
||||
ROUTECTL_SOCKET_UNIT="/etc/systemd/system/dashboards-routectl.socket"
|
||||
ROUTECTL_SERVICE_UNIT="/etc/systemd/system/dashboards-routectl@.service"
|
||||
ROUTECTL_SOCKET_PATH="/run/dashboards-routectl.sock"
|
||||
ROUTECTL_STATE_DIR="/var/lib/dashboards-routectl"
|
||||
|
||||
BIND="127.0.0.1"
|
||||
PORT="9120"
|
||||
@@ -158,6 +193,14 @@ CAPS=""
|
||||
# --force: the re-run guard's opt-in (phase 17 task 03). 0 means a re-run whose generated unit
|
||||
# differs from the installed one refuses after printing the diff.
|
||||
FORCE=0
|
||||
# --with-routectl / --without-routectl (phase 33): the route-switch helper and its two units,
|
||||
# installed beside the agent and never inside its unit. Both off is the default: an install
|
||||
# without the flag is byte-identical to one before the flag existed.
|
||||
WITH_ROUTECTL=0
|
||||
# --allow-writes (phase 33): appends the agent's own --allow-writes to the unit's ExecStart.
|
||||
# It is the agent-side gate for POST /route; the unit's security directives do not move
|
||||
# with it, which is the phase's central safety claim in one flag.
|
||||
ALLOW_WRITES=0
|
||||
MODE="install"
|
||||
|
||||
usage() {
|
||||
@@ -192,6 +235,9 @@ while [ $# -gt 0 ]; do
|
||||
shift 2
|
||||
;;
|
||||
--force) FORCE=1; shift ;;
|
||||
--allow-writes) ALLOW_WRITES=1; shift ;;
|
||||
--with-routectl) WITH_ROUTECTL=1; shift ;;
|
||||
--without-routectl) MODE="routectl-off"; shift ;;
|
||||
--status) MODE="status"; shift ;;
|
||||
--uninstall) MODE="uninstall"; shift ;;
|
||||
-h|--help) usage 0 ;;
|
||||
@@ -233,7 +279,23 @@ fi
|
||||
# The fetch belongs to an install. `--status` probes a port and `--uninstall` deletes files; a
|
||||
# run of either would fetch a private key for nothing and leave it in a temp file.
|
||||
if [ "$FETCH_GIVEN" -eq 3 ] && [ "$MODE" != "install" ]; then
|
||||
echo "--fetch-bundle needs an install (it means nothing to --status or --uninstall)" >&2
|
||||
echo "--fetch-bundle needs an install (it means nothing to --status, --uninstall or --without-routectl)" >&2
|
||||
exit 1
|
||||
fi
|
||||
# One flag each direction, like --as-root/--caps: a run that installs the helper and removes
|
||||
# it is a mistake about what to install, and it gets its own sentence before any write.
|
||||
if [ "$WITH_ROUTECTL" -eq 1 ] && [ "$MODE" = "routectl-off" ]; then
|
||||
echo "--with-routectl and --without-routectl are one flag each direction; run them separately" >&2
|
||||
exit 1
|
||||
fi
|
||||
# --with-routectl installs files; --status probes a port and --uninstall deletes the agent.
|
||||
if [ "$WITH_ROUTECTL" -eq 1 ] && [ "$MODE" != "install" ]; then
|
||||
echo "--with-routectl needs an install (it means nothing to --status or --uninstall)" >&2
|
||||
exit 1
|
||||
fi
|
||||
# --allow-writes rewrites the agent's unit; none of the non-install modes render a unit.
|
||||
if [ "$ALLOW_WRITES" -eq 1 ] && [ "$MODE" != "install" ]; then
|
||||
echo "--allow-writes needs an install (it is an argument to the agent's unit, not to --status, --uninstall or --without-routectl)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -335,6 +397,26 @@ uninstall() {
|
||||
echo "removed $SERVICE (the $AGENT_USER user was left in place; remove it with userdel)"
|
||||
}
|
||||
|
||||
routectl_off() {
|
||||
# The inverse of --with-routectl: the helper and its two units go; the proxy configs and
|
||||
# the helper's state directory stay — the state is the memory of what was switched, and
|
||||
# a route the helper left off is the operator's to switch back on, so the still-off list
|
||||
# prints first and the removal ends with the reminder, not the file list.
|
||||
need_root --without-routectl
|
||||
if [ -f "$ROUTECTL_TARGET" ]; then
|
||||
echo "routes the helper has switched off (these are still off after this removal — switch them back on):"
|
||||
"$ROUTECTL_TARGET" --state "$ROUTECTL_STATE_DIR/state.json" list || true
|
||||
else
|
||||
echo "the helper is not installed; nothing to list"
|
||||
fi
|
||||
systemctl disable --now dashboards-routectl.socket 2>/dev/null || true
|
||||
systemctl disable --now 'dashboards-routectl@.service' 2>/dev/null || true
|
||||
rm -f "$ROUTECTL_SOCKET_UNIT" "$ROUTECTL_SERVICE_UNIT" "$ROUTECTL_TARGET"
|
||||
systemctl daemon-reload
|
||||
echo "removed dashboards-routectl (the proxy configs and $ROUTECTL_STATE_DIR are left in place)"
|
||||
echo "reminder: a route still off in the list above is still off — switch it back on"
|
||||
}
|
||||
|
||||
new_token() {
|
||||
# urlsafe and no padding: the token goes into HOST_TARGETS as URL userinfo
|
||||
# (`name=http://TOKEN@host:9120`), where '+' and '=' are annoying to quote.
|
||||
@@ -590,6 +672,14 @@ RuntimeDirectory=dashboards-agent
|
||||
RuntimeDirectoryMode=0700"
|
||||
fi
|
||||
|
||||
# The write path's one unit change: the flag on ExecStart, and nothing else. The four
|
||||
# security directives below are rendered exactly as before; a diff that shows them moving
|
||||
# is a stop, not a --force (the phase's central safety claim, in one flag).
|
||||
WRITES_EXEC_ARGS=""
|
||||
if [ "$ALLOW_WRITES" -eq 1 ]; then
|
||||
WRITES_EXEC_ARGS=" --allow-writes"
|
||||
fi
|
||||
|
||||
# The kernel-gate variant (--as-root / --caps). Default: the dedicated user and an empty
|
||||
# CapabilityBoundingSet, i.e. the capability-less unit this script has always written.
|
||||
UNIT_USER="$AGENT_USER"
|
||||
@@ -630,7 +720,7 @@ Wants=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$UNIT_USER
|
||||
ExecStart=/usr/bin/env python3 $AGENT_TARGET --host $BIND --port $PORT --token-file ${TOKEN_FILE}${TLS_EXEC_ARGS}
|
||||
ExecStart=/usr/bin/env python3 $AGENT_TARGET --host $BIND --port $PORT --token-file ${TOKEN_FILE}${TLS_EXEC_ARGS}${WRITES_EXEC_ARGS}
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
@@ -677,6 +767,33 @@ SystemCallFilter=@system-service
|
||||
WantedBy=multi-user.target
|
||||
UNIT_FILE
|
||||
|
||||
# The helper and its two units land beside the agent's unit and never inside it: the
|
||||
# re-run guard is the same one, per file — a differing installed helper or unit is a diff
|
||||
# plus exit 1 unless --force, so a routine redeploy cannot reset a helper somebody fixed by
|
||||
# hand. The *guard* for the three runs here, before the agent unit guard, and the *writes*
|
||||
# run after the agent unit write: a refusal does nothing after it — no routectl file, no
|
||||
# agent unit, no daemon-reload, no enable — and an apply writes both before the one reload.
|
||||
ROUTECTL_REFUSED=0
|
||||
if [ "$WITH_ROUTECTL" -eq 1 ]; then
|
||||
for _pair in "$ROUTECTL_SOURCE $ROUTECTL_TARGET" \
|
||||
"$ROUTECTL_SOCKET_UNIT_SRC $ROUTECTL_SOCKET_UNIT" \
|
||||
"$ROUTECTL_SERVICE_UNIT_SRC $ROUTECTL_SERVICE_UNIT"; do
|
||||
_src="${_pair%% *}"
|
||||
_dst="${_pair##* }"
|
||||
[ -f "$_src" ] || { echo "no $_src beside this script" >&2; exit 1; }
|
||||
if [ -f "$_dst" ] && ! cmp -s "$_dst" "$_src"; then
|
||||
if [ "$FORCE" -eq 1 ]; then
|
||||
diff -u "$_dst" "$_src" || true
|
||||
else
|
||||
echo "refusing to overwrite $_dst (installed copy differs); run with --force to apply:"
|
||||
diff -u "$_dst" "$_src" || true
|
||||
ROUTECTL_REFUSED=1
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# No silent clobber (phase 17 task 03). $UNIT is the operator's file as much as this
|
||||
# template's output: it gets edited on the host to fix something the template did not know
|
||||
# (driveripper: User=root plus the two capabilities the kernel-7.2 passthrough gate needs),
|
||||
@@ -699,6 +816,22 @@ UNIT_FILE
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
# Both guards have passed now: the writes are one step. The routectl refusal flag exists
|
||||
# only so this write can be skipped when a routectl file refused above — but the agent
|
||||
# unit guard below exits the run, so a routectl refusal alone must stop the run too:
|
||||
# the refusal is the answer, and a half-apply (agent unit written, routectl not, and no
|
||||
# reload) is the failure it exists to prevent.
|
||||
if [ "$ROUTECTL_REFUSED" -eq 1 ]; then
|
||||
rm -f "$TMP_UNIT"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$WITH_ROUTECTL" -eq 1 ]; then
|
||||
install -d -m 0755 "$(dirname "$ROUTECTL_TARGET")"
|
||||
install -m 0755 "$ROUTECTL_SOURCE" "$ROUTECTL_TARGET"
|
||||
install -m 0644 "$ROUTECTL_SOCKET_UNIT_SRC" "$ROUTECTL_SOCKET_UNIT"
|
||||
install -m 0644 "$ROUTECTL_SERVICE_UNIT_SRC" "$ROUTECTL_SERVICE_UNIT"
|
||||
echo " installed the route-switch helper in $ROUTECTL_TARGET and its two units"
|
||||
fi
|
||||
cat "$TMP_UNIT" > "$UNIT"
|
||||
rm -f "$TMP_UNIT"
|
||||
|
||||
@@ -712,6 +845,24 @@ UNIT_FILE
|
||||
journalctl -u "$SERVICE" -n 20 --no-pager >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$WITH_ROUTECTL" -eq 1 ]; then
|
||||
systemctl enable --now dashboards-routectl.socket
|
||||
if ! systemctl is-active --quiet dashboards-routectl.socket; then
|
||||
echo " dashboards-routectl.socket failed to start — last lines:" >&2
|
||||
journalctl -u dashboards-routectl.socket -n 20 --no-pager >&2
|
||||
exit 1
|
||||
fi
|
||||
# The reachability check the install owes: a socket the agent's own uid cannot connect
|
||||
# to is a feature that fails at 03:00 during an incident, so the check is part of the
|
||||
# install and its output is printed. $UNIT_USER is the service user (the agent runs as
|
||||
# root under --as-root, and root may always connect — the check then passes trivially).
|
||||
if sudo -u "$UNIT_USER" python3 -c 'import socket, sys; s = socket.socket(socket.AF_UNIX); s.connect(sys.argv[1])' "$ROUTECTL_SOCKET_PATH"; then
|
||||
echo " $UNIT_USER can connect to $ROUTECTL_SOCKET_PATH (the whole grant at the file layer)"
|
||||
else
|
||||
echo " $UNIT_USER cannot connect to $ROUTECTL_SOCKET_PATH — the helper is installed but the agent cannot reach it" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ -n "$TLS_EXEC_ARGS" ]; then
|
||||
cat <<NEXT
|
||||
|
||||
@@ -747,4 +898,5 @@ case "$MODE" in
|
||||
install) do_install ;;
|
||||
status) status ;;
|
||||
uninstall) uninstall ;;
|
||||
routectl-off) routectl_off ;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user