eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1#!/usr/bin/env sh
2# πŸ€– ops provision β€” refresh a fleet box's systemd units, and declare which box
3# is the short-link apex.
4#
5# ./deploy/ops provision units [box...] # refresh unit files (lchat bootstrap)
6# ./deploy/ops provision apex <host> [box] # ...and make that box the apex for <host>
7# ./deploy/ops provision status [box...] # read-only: who runs what
8# ./deploy/ops provision operator <name> <box> (--github <handle> | --keys-file <path>)
9# # create/converge an operator account
10#
11# Why this verb exists: **no new systemd unit ever reaches an existing box on
12# its own.** `deploy` and `ship` swap binaries and restart units that already
13# exist; the hourly auto-updater does the same. Only `lchat bootstrap` writes
14# unit files, and nothing re-runs it after the first provision. So a unit added
15# to the repo lands on fresh boxes and silently never appears on the fleet β€”
16# which is exactly how the short-link redirector shipped in the binary and ran
17# on no box at all (issue #905).
18#
19# `provision units` is therefore the missing fleet-side counterpart to
20# `ops dev provision`, and the thing to run after merging any change that adds
21# or edits a unit file.
22#
23# What it does NOT do: touch /data, instances, or versions. `bootstrap` is
24# idempotent and data-safe β€” it writes units, ensures directories and the
25# service user, and restarts the ROUTER. That router restart is the one
26# member-visible effect (a sub-second blip on a retrying router), which is why
27# this is a deliberate verb you run, never something folded into a deploy.
28#
29# NEVER point this at a dev-mesh box. Plain `bootstrap` ENABLES the hourly
30# auto-updater, which would replace the dev mesh's unmerged code with the
31# current stable build β€” `ops dev provision` exists for those boxes and passes
32# --no-update-timer. The only guardrail here is the $LCHAT_BOXES default, so
33# be deliberate when overriding it or naming boxes explicitly.
34#
35# `provision apex` additionally passes --links-apex, which starts the
36# redirector and registers <host> as a route pointing at its socket. Exactly
37# one box in a fleet is the apex; every other box keeps the unit written and
38# disabled. After the first time, the registry route is the source of truth β€”
39# plain `provision units` converges to it, so the apex survives later runs
40# without anyone remembering a flag.
41set -eu
42
43# Same default fleet as doctor/ship, same override.
44BOXES="${LCHAT_BOXES:-elevenbox kepler}"
45# The box that fronts the short-link apex, when `apex` is given no box.
46APEX_BOX="${LCHAT_APEX_BOX:-elevenbox}"
47
48usage() {
49 echo "usage: ops provision units [box...] | ops provision apex <host> [box] |" >&2
50 echo " ops provision status [box...] |" >&2
51 echo " ops provision operator <name> <box> (--github <handle> | --keys-file <path>)" >&2
52 echo >&2
53 echo " units re-run 'lchat bootstrap' so newly-added systemd units land (restarts the router)" >&2
54 echo " apex the same, declaring that box the short-link apex for <host>" >&2
55 echo " status read-only: unit + route state, no changes" >&2
56 echo " operator create/converge a named operator account on <box> (bootstrap --add-operator)" >&2
57 exit 2
58}
59
60# on <box> <command>: privileged box command through the committed sudo
61# allowlist (`/opt/lchat/lchat *`, deploy/sudoers-operators). Operator SSH
62# rides WireGuard and the username comes from ~/.ssh/config β€” root SSH is
63# bootstrap-only, so no user is baked in here.
64on() { box="$1"; shift; ssh -o ConnectTimeout=10 "$box" "$@"; }
65
66# report <box>: the read-only picture β€” is the redirector running here, and
67# which host (if any) routes to it.
68report() {
69 box="$1"
70 printf ' %s:\n' "$box"
71 # is-enabled/is-active exit non-zero for the states we most want to SEE
72 # (disabled, inactive, not-found), and is-enabled adds a stderr explanation
73 # for a missing unit β€” so discard stderr, keep the first line, and only
74 # substitute a word when there was genuinely no output.
75 enabled="$(on "$box" 'systemctl is-enabled lchat-links 2>/dev/null | head -1' || true)"
76 active="$(on "$box" 'systemctl is-active lchat-links 2>/dev/null | head -1' || true)"
77 printf ' lchat-links %s / %s\n' "${enabled:-not-found}" "${active:-inactive}"
78 route="$(on "$box" "sudo /opt/lchat/lchat instances 2>/dev/null | grep -F '/run/lchat/links.sock' || true")"
79 if [ -n "$route" ]; then
80 printf ' apex route %s\n' "$(printf '%s' "$route" | awk '{print $1}')"
81 else
82 printf ' apex route (none β€” not the short-link apex)\n'
83 fi
84}
85
86cmd="${1:-}"
87[ $# -gt 0 ] && shift
88case "$cmd" in
89
90units)
91 boxes="${*:-$BOXES}"
92 echo "ops provision: refreshing systemd units on: $boxes"
93 echo " (this restarts the router on each β€” a sub-second blip, not a data operation)"
94 for box in $boxes; do
95 echo "ops provision: $box"
96 on "$box" 'sudo /opt/lchat/lchat bootstrap'
97 report "$box"
98 done
99 echo
100 echo "note: this opened box SSH sessions β€” if this session hasn't written its"
101 echo " access-ledger why-entry yet: ./deploy/ops ledger \"...\""
102 ;;
103
104apex)
105 host="${1:-}"; [ -n "$host" ] || usage
106 shift
107 box="${1:-$APEX_BOX}"
108
109 # The apex is a real, public hostname whose DNS must already point at this
110 # box β€” the router issues its certificate lazily on first handshake, and an
111 # apex is NOT covered by a *.domain wildcard, so this is the one part the
112 # script can't do for you. Say so plainly rather than failing later at TLS.
113 echo "ops provision: making $box the short-link apex for $host"
114 echo " prerequisite: $host must already resolve to $box, with its OWN A record."
115 # Only worth spelling out where a covering wildcard plausibly exists: a host
116 # under a parent domain (dev.11ch.at), not a registrable apex (11ch.at),
117 # where "a *.ch.at wildcard" would be nonsense.
118 case "$host" in
119 *.*.*) echo " A *.${host#*.} wildcard does NOT cover $host β€” an apex never is." ;;
120 esac
121 on "$box" "sudo /opt/lchat/lchat bootstrap --links-apex $host"
122 report "$box"
123
124 echo "ops provision: checking https://$host/ ..."
125 # The certificate is issued on the first handshake, so a cold apex can take a
126 # few seconds. A TLS failure here (rather than an HTTP status) almost always
127 # means DNS doesn't point at this box, or the route didn't register.
128 apex=""
129 for _ in 1 2 3 4 5 6; do
130 apex="$(curl -sS -o /dev/null -m 15 -w '%{http_code} -> %{redirect_url}' "https://$host/" 2>&1 || true)"
131 case "$apex" in 30*) break ;; esac
132 sleep 5
133 done
134 echo " apex: $apex"
135 case "$apex" in
136 30*) echo " (a 30x to the marketing site is the redirector answering β€” short links are live)" ;;
137 *) echo "ops provision: $host did not redirect." >&2
138 echo " TLS error -> DNS probably doesn't point at $box, or the route didn't register" >&2
139 echo " 404/timeout -> check: ssh $box journalctl -u lchat-links -n 40 --no-pager" >&2
140 exit 1 ;;
141 esac
142 echo
143 echo "note: this opened box SSH sessions β€” if this session hasn't written its"
144 echo " access-ledger why-entry yet: ./deploy/ops ledger \"...\""
145 ;;
146
147status)
148 boxes="${*:-$BOXES}"
149 echo "ops provision: short-link state"
150 for box in $boxes; do report "$box"; done
151 ;;
152
153# The later-joiner half of operator setup (docs/instance-router.md Β§
154# Hardening): the first operators were created in the box's one root
155# session, and a box hardened before an operator joined has no root shell
156# left β€” kepler had James's WireGuard peer but no account. This drives the
157# allowlist-reachable `bootstrap --add-operator`, keys piped on stdin.
158# The account is only HALF of access: the operator's WireGuard peer must
159# already be on the box's wg0 (see RUNBOOK Β§ Box access), or nothing routes.
160operator)
161 name="${1:-}"; box="${2:-}"; src="${3:-}"; arg="${4:-}"
162 [ -n "$name" ] && [ -n "$box" ] && [ -n "$arg" ] || usage
163 case "$name" in
164 *[!a-z0-9-]*|-*) echo "ops provision: operator name must be lowercase [a-z0-9-]" >&2; exit 1 ;;
165 esac
166 case "$src" in
167 --github) keys="$(curl -fsS "https://github.com/$arg.keys")" ;;
168 --keys-file) keys="$(cat "$arg")" ;;
169 *) usage ;;
170 esac
171 [ -n "$keys" ] || { echo "ops provision: no keys from $src $arg" >&2; exit 1; }
172 echo "ops provision: installing for '$name' on $box:"
173 printf '%s\n' "$keys" | awk '{print " " $1 " ..." substr($2, length($2)-7) " " $3}'
174 # Public keys fetched from GitHub are an INTAKE, not an identity proof β€”
175 # same caveat as the vault recipients: confirm over the messenger that the
176 # keys are currently theirs.
177 [ "$src" = "--github" ] && echo " (from github.com/$arg.keys β€” confirm with them over the messenger that these are current)"
178 printf '%s\n' "$keys" | on "$box" "sudo /opt/lchat/lchat bootstrap --add-operator $name"
179 echo "ops provision: verifying:"
180 on "$box" "id $name"
181 echo
182 echo "note: their WireGuard peer must already be on $box's wg0 for SSH to route"
183 echo " (sudo wg show wg0 β€” see RUNBOOK Β§ Box access to add one)."
184 echo "note: this opened box SSH sessions β€” if this session hasn't written its"
185 echo " access-ledger why-entry yet: ./deploy/ops ledger \"...\""
186 ;;
187
188-h|--help|help|"") usage ;;
189*) echo "ops provision: unknown subcommand '$cmd'" >&2; usage ;;
190esac
191