NA sees every campus; SSH from the admin laptop to every machine
Campuses set up from NA's install line tell NA where they are and learn nothing back (no campus ever shows another). NA alone reads each campus's key states every two seconds and keeps two pages in its Companion (added at the end, 98 and 99, since every other page is in use): CAMPUSES, a key per campus (its ALL key, purple when it stops answering), and CAMPUS, the tapped campus's status page drawn live in the same places, view only. Tried on NA with a test campus pointing at NA itself, then removed. tools/fleet.py: an Apple Remote Desktop command that turns on Remote Login, adds the laptop's key and reports each Mac to NA, and the laptop SSH shortcuts made from those reports (tried on NA Translation). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+9
-4
@@ -153,9 +153,13 @@ def wait_applied(applier, limit=240):
|
||||
return applier.status or {}
|
||||
|
||||
|
||||
def setup(data_dir, cid, name=None, say=print):
|
||||
def setup(data_dir, cid, name=None, say=print, home=None):
|
||||
"""The first setup of a campus (or, when it is set up already, a look for what is new)."""
|
||||
from settings import server
|
||||
from settings import server, campuses
|
||||
if home: # where it was installed from: the campus list lives there
|
||||
data = campuses.load(data_dir)
|
||||
data["home"] = home
|
||||
campuses.save(data_dir, data)
|
||||
site = server.Site(data_dir)
|
||||
cfg = site.load()
|
||||
if not is_fresh(cfg):
|
||||
@@ -335,7 +339,7 @@ if [[ -n "$SUPPORT" ]]; then
|
||||
fi
|
||||
zsh install.sh --site %(cid)s
|
||||
cd "$HOME/Library/Application Support/status-panel"
|
||||
/usr/bin/python3 -m settings.campus setup --id %(cid)s --name %(qname)s
|
||||
/usr/bin/python3 -m settings.campus setup --id %(cid)s --name %(qname)s --home "$SRC"
|
||||
if ! /usr/bin/nc -z -G 2 127.0.0.1 22 >/dev/null 2>&1; then
|
||||
echo
|
||||
echo "One more thing: turn on Remote Login (System Settings > General > Sharing), so North Attleboro can help remotely."
|
||||
@@ -411,9 +415,10 @@ def main(argv=None):
|
||||
ap.add_argument("--id", default="new")
|
||||
ap.add_argument("--name")
|
||||
ap.add_argument("--data-dir", default=PARENT)
|
||||
ap.add_argument("--home", help="the settings page this campus was installed from (the campus list lives there)")
|
||||
a = ap.parse_args(argv)
|
||||
if a.what == "setup":
|
||||
setup(a.data_dir, a.id, a.name)
|
||||
setup(a.data_dir, a.id, a.name, home=a.home)
|
||||
elif a.what == "look":
|
||||
cfg = sp.load_config(os.path.join(a.data_dir, "config.json"))
|
||||
inv = discover.look(cfg, a.data_dir)
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
"""Every campus's status, on North Attleboro's Companion only.
|
||||
|
||||
Each other campus's settings page tells North Attleboro's (the hub: a campus with no "home") where it
|
||||
is, and learns nothing back: no other campus ever sees another campus (2026-10-20, the user: "NA
|
||||
should be the only one that sees every single location's status"). Only the hub, every two seconds,
|
||||
copies each campus's key states (what that campus's watchdog draws, /api/state) into its own
|
||||
Companion as custom variables sp_state_<id> / sp_hb_<id>, and keeps two kinds of page there:
|
||||
|
||||
CAMPUSES page one key per campus: that campus's ALL key, with its name; tap = its status page
|
||||
CAMPUS page whichever campus was tapped, as its own status page looks (same places, same
|
||||
colours, live), view only: holds and presses are for that campus's own deck.
|
||||
The ALARMS corner goes back to the CAMPUSES page.
|
||||
|
||||
Two pages, whatever the number of campuses (NA's Companion has 97 pages of its own; Companion stops
|
||||
at 99): the CAMPUS page's keys are cells ("c_1_3"), and the hub fills them from the tapped campus's
|
||||
layout (sp_view says which). A campus that stops answering goes purple. Pages are only written
|
||||
where nothing a person made sits (push_page.safe_target): an empty page, else one added at the
|
||||
end. State: campuses.json in the data folder.
|
||||
"""
|
||||
import gzip
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
from settings import PARENT # noqa: F401 (puts the project folder on sys.path)
|
||||
from settings import discover
|
||||
import make_page
|
||||
import push_page
|
||||
import status_panel as sp
|
||||
|
||||
FILE = "campuses.json"
|
||||
POLL, REFRESH = 2.0, 60.0
|
||||
SHORT = {"na": "NA", "ab": "AB", "sc": "SC", "ws": "WS"}
|
||||
MIRROR_MARK = "STATUS tile" # copied keys keep the tile marker, so a later rebuild may overwrite them
|
||||
|
||||
|
||||
def load(data_dir):
|
||||
try:
|
||||
with open(os.path.join(data_dir, FILE)) as f:
|
||||
return json.load(f)
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
|
||||
|
||||
def save(data_dir, data):
|
||||
sp.write_json(os.path.join(data_dir, FILE), data)
|
||||
|
||||
|
||||
def short(cid, name=""):
|
||||
return SHORT.get(cid) or (name or cid)[:4].upper()
|
||||
|
||||
|
||||
def my_url(cfg):
|
||||
w = (discover.this_mac().get("wired") or {}).get("ip") or "127.0.0.1"
|
||||
return "http://%s:%d" % (w, int((cfg.get("site") or {}).get("settings_port", 8020)))
|
||||
|
||||
|
||||
def me(cfg, url=None):
|
||||
site = cfg.get("site") or {}
|
||||
return {"id": site.get("id") or "new", "name": site.get("name") or "This campus", "url": url or my_url(cfg)}
|
||||
|
||||
|
||||
def layout_sha(cfg):
|
||||
keys = [(t.get("id"), t.get("label"), t.get("pos"), t.get("kind"), t.get("members")) for t in cfg.get("tiles", [])]
|
||||
return hashlib.sha256(json.dumps([keys, cfg.get("page_grid"), cfg.get("variables")], sort_keys=True, default=str).encode()).hexdigest()[:16]
|
||||
|
||||
|
||||
def state_now(site):
|
||||
"""What /api/state answers: this campus's key states as its watchdog draws them, right now."""
|
||||
cfg = site.load()
|
||||
names = cfg.get("variables") or {}
|
||||
comp = site.comp
|
||||
return {"id": (cfg.get("site") or {}).get("id"), "name": (cfg.get("site") or {}).get("name"),
|
||||
"state": comp.get_custom(names.get("state", "sp_state")) or "", "hb": comp.get_custom(names.get("heartbeat", "sp_hb")) or "",
|
||||
"layout": layout_sha(cfg), "time": int(time.time())}
|
||||
|
||||
|
||||
def http_json(url, body=None, timeout=3):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"} if data else {}, method="POST" if data else "GET")
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return json.loads(r.read().decode("utf-8", "replace"))
|
||||
|
||||
|
||||
# --- the pages (the hub only) -----------------------------------------------------------
|
||||
|
||||
VIEW = {"state": "sp_state_view", "heartbeat": "sp_hb_view"}
|
||||
SELECT = "sp_view"
|
||||
CELLS = [(r, c) for r in range(4) for c in range(1, 8)]
|
||||
|
||||
|
||||
def cell_id(r, c):
|
||||
return "c_%d_%d" % (r, c)
|
||||
|
||||
|
||||
def view_state(state, keys):
|
||||
"""A campus's key states, moved onto the cells its keys sit in (empty cells blank)."""
|
||||
out = {cell_id(r, c): {"x": "", "b": 0, "b2": 0, "f": 0xFFFFFF, "f2": 0xFFFFFF, "s": 14} for r, c in CELLS}
|
||||
for k in keys:
|
||||
r, c = k["pos"]
|
||||
if (r, c) in CELLS and k["id"] in state:
|
||||
out[cell_id(r, c)] = state[k["id"]]
|
||||
return out
|
||||
|
||||
|
||||
def build_view(local_cfg, page, overview_page):
|
||||
"""The CAMPUS page: a key per cell, drawn from sp_state_view; the ALARMS corner goes back."""
|
||||
opts = make_page.settings(local_cfg)
|
||||
controls = make_page.nav_controls(local_cfg)
|
||||
for r, c in CELLS:
|
||||
layers = make_page.layers(cell_id(r, c), {}, VIEW, "''", opts)
|
||||
make_page.place(controls, (r, c), {
|
||||
"type": "button-layered", "style": {"layers": layers},
|
||||
"options": {"stepProgression": "auto", "stepExpression": "", "rotaryActions": False, "canModifyStyleInApis": True,
|
||||
"notes": "STATUS tile 'campus view %d/%d' - the tapped campus's key here, view only (hold it on that campus's own deck)." % (r, c)},
|
||||
"feedbacks": [], "steps": {"0": {"action_sets": {"down": [], "up": []}, "options": {"runWhileHeld": []}}}, "localVariables": []})
|
||||
make_page.place(controls, (3, 0), make_page.static_button("campus_back", "ALL\\nCAMPUSES", 0x37474F, 0xFFFFFF, overview_page,
|
||||
"Tap: back to the CAMPUSES page (page %d)." % overview_page, "STATUS jump key"))
|
||||
return make_page.page_export("%d\\nCampus" % page, page, controls, local_cfg)
|
||||
|
||||
|
||||
def select_action(cid):
|
||||
return {"type": "action", "id": make_page.stable_id("campus_" + cid, "select"), "definitionId": "custom_variable_set_value",
|
||||
"connectionId": "internal", "options": {"name": make_page.prop(SELECT), "value": make_page.prop(cid)}, "children": {}}
|
||||
|
||||
|
||||
def campus_key(cid, name, names, actions, note, local_cfg):
|
||||
"""One CAMPUSES key: that campus's ALL key with its name on top."""
|
||||
opts = make_page.settings(local_cfg)
|
||||
tag = short(cid, name)
|
||||
stale_text = "`%s\\n?`" % tag
|
||||
layers = make_page.layers("all", {}, names, stale_text, opts)
|
||||
text = next(layer for layer in layers if layer["type"] == "text")
|
||||
stale = "$(internal:time_unix) - $(custom:%s) > %d" % (names["heartbeat"], opts["stale"])
|
||||
text["text"] = make_page.prop("%s ? %s : `%s\\n${jsonpath($(custom:%s), '$.all.x')}`" % (stale, stale_text, tag, names["state"]), True)
|
||||
text["fontsize"] = make_page.prop("auto")
|
||||
return {"type": "button-layered", "style": {"layers": layers},
|
||||
"options": {"stepProgression": "auto", "stepExpression": "", "rotaryActions": False, "canModifyStyleInApis": True,
|
||||
"notes": "STATUS tile 'campus %s' - %s's ALL key. %s" % (cid, name, note)},
|
||||
"feedbacks": [], "steps": {"0": {"action_sets": {"down": [], "up": actions}, "options": {"runWhileHeld": []}}},
|
||||
"localVariables": []}
|
||||
|
||||
|
||||
def build_overview(local_cfg, campuses, page, view_page):
|
||||
controls = make_page.nav_controls(local_cfg)
|
||||
mine = (local_cfg.get("site") or {}).get("id")
|
||||
for cid, cell in zip(sorted(campuses, key=lambda c: (c != mine, c)), CELLS):
|
||||
name = campuses[cid].get("name", cid)
|
||||
if cid == mine:
|
||||
key = campus_key(cid, name, local_cfg["variables"], [make_page.set_page_action("campus_" + cid, "go", local_cfg["page"])],
|
||||
"Tap: its status page (page %d)." % local_cfg["page"], local_cfg)
|
||||
else:
|
||||
key = campus_key(cid, name, {"state": "sp_state_" + cid, "heartbeat": "sp_hb_" + cid},
|
||||
[select_action(cid), make_page.set_page_action("campus_" + cid, "go", view_page)],
|
||||
"Tap: its status page, live (page %d)." % view_page, local_cfg)
|
||||
make_page.place(controls, cell, key)
|
||||
return make_page.page_export("%d\\nCampuses" % page, page, controls, local_cfg)
|
||||
|
||||
|
||||
def ensure_variables(cfg, ids):
|
||||
from settings import campus
|
||||
have, _ = campus.companion_state(cfg)
|
||||
want = [n for cid in ids for n in ("sp_state_" + cid, "sp_hb_" + cid) if n not in have]
|
||||
want += [n for n in (VIEW["state"], VIEW["heartbeat"], SELECT) if n not in have]
|
||||
if not want:
|
||||
return
|
||||
comp = cfg.get("companion") or {}
|
||||
ws = push_page.WebSocket(comp.get("host", "127.0.0.1"), int(comp.get("port", 8000)), "/trpc")
|
||||
try:
|
||||
api = push_page.Trpc(ws)
|
||||
for n in want:
|
||||
api.mutation("customVariables.create", {"name": n, "defaultVal": "0" if n.startswith("sp_hb_") else ""})
|
||||
finally:
|
||||
ws.close()
|
||||
|
||||
|
||||
def free_page(cfg, taken, start=21):
|
||||
"""The first page from `start` that holds nothing but Companion's own page keys."""
|
||||
comp = cfg.get("companion") or {}
|
||||
host, port = comp.get("host", "127.0.0.1"), int(comp.get("port", 8000))
|
||||
for page in range(start, 100):
|
||||
if page in taken:
|
||||
continue
|
||||
if not exists(host, port, page):
|
||||
break
|
||||
try:
|
||||
export = push_page.fetch_page(host, port, page)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
ctrls = (export.get("page") or {}).get("controls") or {}
|
||||
if not any(ctl and ctl.get("type") not in push_page.NAV_TYPES for row in ctrls.values() for ctl in (row or {}).values()):
|
||||
return page
|
||||
# None empty: a new page at the end (Companion's own page keys on it), never in between.
|
||||
last = max(p for p in range(1, 100) if exists(host, port, p))
|
||||
if last >= 99:
|
||||
raise RuntimeError("Companion has 99 pages and none is empty: free one for the campuses")
|
||||
ws = push_page.WebSocket(host, port, "/trpc")
|
||||
try:
|
||||
push_page.Trpc(ws).mutation("pages.insert", {"asPageNumber": last + 1, "pageNames": ["CAMPUSES"]})
|
||||
finally:
|
||||
ws.close()
|
||||
return last + 1
|
||||
|
||||
|
||||
def exists(host, port, page):
|
||||
try:
|
||||
push_page.fetch_page(host, port, page)
|
||||
return True
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def push(cfg, page, export):
|
||||
comp = cfg.get("companion") or {}
|
||||
host, port = comp.get("host", "127.0.0.1"), int(comp.get("port", 8000))
|
||||
ok, why = push_page.safe_target(push_page.fetch_page(host, port, page))
|
||||
if not ok:
|
||||
raise RuntimeError("page %d: %s" % (page, why))
|
||||
with tempfile.NamedTemporaryFile(suffix=".companionconfig", delete=False) as f:
|
||||
f.write(gzip.compress(json.dumps(export).encode()))
|
||||
path = f.name
|
||||
try:
|
||||
push_page.push(host, port, page, path, export["page"]["name"])
|
||||
finally:
|
||||
os.remove(path)
|
||||
|
||||
|
||||
# --- the loops --------------------------------------------------------------------------
|
||||
|
||||
class Mirror:
|
||||
def __init__(self, site):
|
||||
self.site = site
|
||||
self.remote = {} # id -> last /api/state answer
|
||||
self.keys = {} # id -> [{"id", "pos"}] of its status page (from its /api/config)
|
||||
|
||||
@staticmethod
|
||||
def is_hub(data):
|
||||
return not data.get("home")
|
||||
|
||||
def sync_registry(self):
|
||||
"""A campus: tell the hub where this settings page is (it learns nothing back). The hub: know itself."""
|
||||
cfg = self.site.load()
|
||||
data = load(self.site.data_dir)
|
||||
mine = me(cfg)
|
||||
home = data.get("home")
|
||||
if home and home.rstrip("/") != mine["url"]:
|
||||
try:
|
||||
http_json(home.rstrip("/") + "/api/campus/hello", mine)
|
||||
data.pop("error", None)
|
||||
except (OSError, ValueError) as e:
|
||||
data["error"] = "hub %s: %s" % (home, e)
|
||||
data["list"] = {mine["id"]: mine}
|
||||
else:
|
||||
data.setdefault("list", {})[mine["id"]] = mine
|
||||
save(self.site.data_dir, data)
|
||||
return data
|
||||
|
||||
def taken_pages(self, cfg):
|
||||
t = {cfg.get("page"), (cfg.get("restart_page") or {}).get("page"), (cfg.get("ready_page") or {}).get("page")}
|
||||
return {p for p in t if p}
|
||||
|
||||
def refresh_pages(self):
|
||||
"""The hub: each campus's layout (when it changed), the CAMPUS page once, the CAMPUSES page when the roster changes."""
|
||||
cfg = self.site.load()
|
||||
data = self.sync_registry()
|
||||
if not self.is_hub(data):
|
||||
return data
|
||||
mine = (cfg.get("site") or {}).get("id")
|
||||
others = {cid: e for cid, e in data.get("list", {}).items() if cid != mine}
|
||||
if not others and not (data.get("pages") or {}).get("overview"):
|
||||
return data # no other campus yet: no pages needed
|
||||
pages = data.setdefault("pages", {})
|
||||
layouts = data.setdefault("layouts", {})
|
||||
for cid, e in sorted(others.items()):
|
||||
st = self.remote.get(cid)
|
||||
if st and (layouts.get(cid) != st.get("layout") or cid not in self.keys):
|
||||
remote_cfg = http_json(e["url"].rstrip("/") + "/api/config", timeout=8)["config"]
|
||||
self.keys[cid] = [{"id": t["id"], "pos": t["pos"]} for t in remote_cfg.get("tiles", []) if t.get("pos")]
|
||||
layouts[cid] = st.get("layout")
|
||||
ensure_variables(cfg, list(others))
|
||||
taken = self.taken_pages(cfg) | {p for p in pages.values() if p}
|
||||
for which in ("overview", "view"):
|
||||
if not pages.get(which):
|
||||
pages[which] = free_page(cfg, taken)
|
||||
taken.add(pages[which])
|
||||
built = layout_sha(cfg)
|
||||
if data.get("view_built") != built:
|
||||
push(cfg, pages["view"], build_view(cfg, pages["view"], pages["overview"]))
|
||||
data["view_built"] = built
|
||||
roster = sorted(data["list"])
|
||||
if data.get("overview_roster") != roster or data.get("overview_built") != built:
|
||||
push(cfg, pages["overview"], build_overview(cfg, data["list"], pages["overview"], pages["view"]))
|
||||
data["overview_roster"], data["overview_built"] = roster, built
|
||||
data.pop("error", None)
|
||||
save(self.site.data_dir, data)
|
||||
return data
|
||||
|
||||
def poll(self):
|
||||
"""The hub: copy each other campus's key states into this Companion."""
|
||||
cfg = self.site.load()
|
||||
data = load(self.site.data_dir)
|
||||
if not self.is_hub(data):
|
||||
return
|
||||
mine = (cfg.get("site") or {}).get("id")
|
||||
others = {cid: e for cid, e in (data.get("list") or {}).items() if cid != mine and e.get("url")}
|
||||
threads = []
|
||||
|
||||
def one(cid, url):
|
||||
try:
|
||||
st = http_json(url.rstrip("/") + "/api/state", timeout=1.5)
|
||||
except (OSError, ValueError):
|
||||
return # not answering: its heartbeat goes stale, its keys purple
|
||||
self.remote[cid] = st
|
||||
if (data.get("pages") or {}).get("overview"):
|
||||
self.site.comp.set_custom("sp_state_" + cid, st.get("state") or "{}")
|
||||
self.site.comp.set_custom("sp_hb_" + cid, str(st.get("hb") or "0"))
|
||||
for cid, e in others.items():
|
||||
t = threading.Thread(target=one, args=(cid, e["url"]), daemon=True)
|
||||
t.start()
|
||||
threads.append(t)
|
||||
for t in threads:
|
||||
t.join(2)
|
||||
# The CAMPUS page shows the tapped campus: its keys moved onto their cells.
|
||||
pick = (self.site.comp.get_custom(SELECT) or "").strip()
|
||||
st = self.remote.get(pick)
|
||||
if st and pick in self.keys and (data.get("pages") or {}).get("view"):
|
||||
try:
|
||||
state = json.loads(st.get("state") or "{}")
|
||||
except ValueError:
|
||||
state = {}
|
||||
self.site.comp.set_custom(VIEW["state"], json.dumps(view_state(state, self.keys[pick]), separators=(",", ":")))
|
||||
self.site.comp.set_custom(VIEW["heartbeat"], str(st.get("hb") or "0"))
|
||||
|
||||
|
||||
def loop(site, stop):
|
||||
m = Mirror(site)
|
||||
site.mirror = m
|
||||
last = 0.0
|
||||
stop.wait(15)
|
||||
while not stop.is_set():
|
||||
try:
|
||||
m.poll()
|
||||
if time.time() - last > REFRESH:
|
||||
last = time.time()
|
||||
m.refresh_pages()
|
||||
except Exception as e: # noqa: BLE001 - the page must keep running
|
||||
data = load(site.data_dir)
|
||||
data["error"] = str(e)[:300]
|
||||
save(site.data_dir, data)
|
||||
stop.wait(POLL)
|
||||
+34
-1
@@ -40,7 +40,7 @@ from settings import builders
|
||||
from settings import scripts
|
||||
from settings import outlet as outlets
|
||||
from settings import keyguide, probes
|
||||
from settings import campus, discover
|
||||
from settings import campus, campuses, discover
|
||||
|
||||
log = logging.getLogger("status-settings")
|
||||
UI = os.path.join(HERE, "ui")
|
||||
@@ -696,6 +696,15 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
return self.send(200, campus.join_mac(site.load(), self.base_url()), "text/plain; charset=utf-8")
|
||||
if path == "/join.ps1":
|
||||
return self.send(200, campus.join_windows(site.load(), self.base_url()), "text/plain; charset=utf-8")
|
||||
if path == "/api/state":
|
||||
return self.send(200, campuses.state_now(site))
|
||||
if path == "/api/campuses":
|
||||
# The hub's own view (its settings page). A campus only ever lists itself.
|
||||
data = campuses.load(site.data_dir)
|
||||
return self.send(200, {"list": data.get("list") or {}, "pages": data.get("pages") or {} if not data.get("home") else {},
|
||||
"error": data.get("error"), "hub": not data.get("home")})
|
||||
if path == "/api/fleet":
|
||||
return self.send(200, {"machines": list((site.read_json("fleet.json", {}) or {}).values())})
|
||||
if path == "/api/discover":
|
||||
cfg = site.load()
|
||||
base = self.base_url()
|
||||
@@ -873,6 +882,29 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
log.info("joined: %s (%s, %s) from %s", entry["name"], entry["os"], entry["user"], self.client_address[0])
|
||||
site.discover_poke.set()
|
||||
return self.send(200, {"ok": True})
|
||||
if path == "/api/campus/hello":
|
||||
# Another campus says where its settings page is (the hub keeps the list for everyone).
|
||||
cid = str(body.get("id") or "")
|
||||
url = str(body.get("url") or "")
|
||||
if not re.fullmatch(r"[a-z0-9]{1,8}", cid) or not re.fullmatch(r"http://\d{1,3}(\.\d{1,3}){3}:\d{2,5}", url) \
|
||||
or not allowed_client(url[7:].split(":")[0]):
|
||||
return self.send(400, {"error": "an id like ws and an address like http://10.5.1.1:8020"})
|
||||
data = campuses.load(site.data_dir)
|
||||
data.setdefault("list", {})[cid] = {"id": cid, "name": str(body.get("name") or cid)[:40], "url": url}
|
||||
campuses.save(site.data_dir, data)
|
||||
return self.send(200, {"ok": True})
|
||||
if path == "/api/fleet/hello":
|
||||
# A machine anywhere (any campus) says it now takes the admin laptop's key: kept for the
|
||||
# laptop's SSH shortcuts (tools/fleet.py). Nothing here acts on it.
|
||||
try:
|
||||
entry = discover.clean_hello(body, self.client_address[0])
|
||||
except (ValueError, TypeError) as e:
|
||||
return self.send(400, {"error": str(e)})
|
||||
fleet = site.read_json("fleet.json", {}) or {}
|
||||
fleet["%s|%s" % (entry["name"], (entry["ips"] or ["?"])[0])] = entry
|
||||
sp.write_json(os.path.join(site.data_dir, "fleet.json"), fleet)
|
||||
log.info("fleet: %s (%s, %s) from %s", entry["name"], entry["os"], entry["user"], self.client_address[0])
|
||||
return self.send(200, {"ok": True})
|
||||
if path == "/api/editing/check":
|
||||
return self.send(200, site.check_code(self.client_address[0], str(body.get("code", ""))))
|
||||
if path == "/api/machines/probe":
|
||||
@@ -1444,6 +1476,7 @@ def main(argv=None):
|
||||
stop = threading.Event()
|
||||
threading.Thread(target=rebind_loop, args=(site, servers, port, stop), daemon=True).start()
|
||||
threading.Thread(target=campus.loop, args=(site, stop, site.discover_poke), name="auto-setup", daemon=True).start()
|
||||
threading.Thread(target=campuses.loop, args=(site, stop), name="campuses", daemon=True).start()
|
||||
try:
|
||||
while True:
|
||||
time.sleep(3600)
|
||||
|
||||
Reference in New Issue
Block a user