Development

This commit is contained in:
Matthew Grotke 2026-05-24 01:13:45 -04:00
parent e72676eac5
commit 80390334fb
2 changed files with 75 additions and 45 deletions

View file

@ -152,49 +152,59 @@ def check_services(data):
iface = derive_interface(vlan, data)
name = _vlan_service_name(vlan, iface)
units.append({"id": name, "name": name,
"expected_active": "active", "expected_enabled": "enabled"})
"expected_active": "active", "expected_enabled": "enabled",
"severity": "error"})
units.append({"id": f"{BLIST_TIMER_NAME}.timer",
"name": f"{BLIST_TIMER_NAME}.timer",
"expected_active": "active", "expected_enabled": "enabled"})
"expected_active": "active", "expected_enabled": "enabled",
"severity": "warning"})
units.append({"id": NAT_SERVICE_NAME,
"name": NAT_SERVICE_NAME,
"expected_active": "inactive",
"expected_enabled": "enabled"})
"expected_enabled": "enabled",
"severity": "error"})
units.append({"id": f"{STATUS_TIMER_NAME}.timer",
"name": f"{STATUS_TIMER_NAME}.timer",
"expected_active": "active", "expected_enabled": "enabled"})
"expected_active": "active", "expected_enabled": "enabled",
"severity": "warning"})
if DASHB_QUEUE_FILE.exists():
units.append({"id": f"{DASHB_TIMER_NAME}.timer",
"name": f"{DASHB_TIMER_NAME}.timer",
"expected_active": "active", "expected_enabled": "enabled"})
"expected_active": "active", "expected_enabled": "enabled",
"severity": "error"})
has_ddns = any(p.get("enabled") for p in data.get("ddns", {}).get("providers", []))
exp_ddns_active = "active" if has_ddns else "inactive"
exp_ddns_enabled = "enabled" if has_ddns else "not-found"
units.append({"id": f"{DDNS_TIMER_NAME}.timer",
"name": f"{DDNS_TIMER_NAME}.timer",
"expected_active": exp_ddns_active, "expected_enabled": exp_ddns_enabled})
"expected_active": exp_ddns_active, "expected_enabled": exp_ddns_enabled,
"severity": "warning"})
exp_fr_active = "active" if _radius_enabled(data) else "inactive"
exp_fr_enabled = "enabled" if _radius_enabled(data) else "disabled"
units.append({"id": "freeradius", "name": "freeradius",
"expected_active": exp_fr_active,
"expected_enabled": exp_fr_enabled})
"expected_enabled": exp_fr_enabled,
"severity": "error"})
exp_av_active = "active" if _avahi_enabled(data) else "inactive"
exp_av_enabled = "enabled" if _avahi_enabled(data) else "disabled"
units.append({"id": "avahi-daemon", "name": "avahi-daemon",
"expected_active": exp_av_active,
"expected_enabled": exp_av_enabled})
"expected_enabled": exp_av_enabled,
"severity": "warning"})
units.append({"id": "chrony", "name": "chrony",
"expected_active": "active", "expected_enabled": "enabled"})
"expected_active": "active", "expected_enabled": "enabled",
"severity": "warning"})
units.append({"id": "systemd-networkd", "name": "systemd-networkd",
"expected_active": "active", "expected_enabled": "enabled"})
"expected_active": "active", "expected_enabled": "enabled",
"severity": "error"})
for u in units:
active, enabled = _sysctl_query(u["id"])
@ -204,15 +214,16 @@ def check_services(data):
enabled_ok = enabled == exp_enabled
status = "ok" if (active_ok and enabled_ok) else "problem"
results.append({
"id": u["id"],
"name": u["name"],
"active": active,
"enabled": enabled,
"expected_active": exp_active,
"id": u["id"],
"name": u["name"],
"active": active,
"enabled": enabled,
"expected_active": exp_active,
"expected_enabled": exp_enabled,
"active_ok": active_ok,
"enabled_ok": enabled_ok,
"status": status,
"active_ok": active_ok,
"enabled_ok": enabled_ok,
"severity": u.get("severity", "error"),
"status": status,
})
return results
@ -235,7 +246,7 @@ def check_configurations(data):
if not exists:
return _problem(id_, name, severity,
f"{path} does not exist.",
suggestion or f"Run sudo python3 core.py --apply to create it.")
suggestion or f"Run `sudo python3 core.py --apply` to create it.")
return _ok(id_, name)
# --- nftables tables ---
@ -253,7 +264,7 @@ def check_configurations(data):
f"nftables table {tbl}",
"error",
f"nftables table '{tbl}' is missing.",
"Run sudo python3 core.py --apply to rebuild firewall rules."))
"Run `sudo python3 core.py --apply` to rebuild firewall rules."))
except Exception:
results.append(_problem("nft_tables", "nftables tables", "error",
"Could not query nftables (nft not available or failed)."))
@ -275,7 +286,7 @@ def check_configurations(data):
results.append(_problem(
"nft_docker_bridges", "nftables Docker bridge rules", "warning",
f"Container bridge(s) {', '.join(missing)} have no nftables forward rules.",
"Run sudo python3 core.py --apply to add the missing rules."))
"Run `sudo python3 core.py --apply` to add the missing rules."))
else:
results.append(_ok("nft_docker_bridges", "nftables Docker bridge rules"))
except Exception:
@ -291,11 +302,11 @@ def check_configurations(data):
if state is None:
results.append(_problem(id_, name, "error",
f"Interface {iface} does not exist in /sys/class/net/.",
"Run sudo python3 core.py --apply to configure network interfaces."))
"Run `sudo python3 core.py --apply` to configure network interfaces."))
elif state != "up":
results.append(_problem(id_, name, "error",
f"Interface {iface} operstate is '{state}' (expected 'up').",
"Check systemd-networkd: sudo systemctl status systemd-networkd"))
"Check systemd-networkd: `sudo systemctl status systemd-networkd`"))
else:
results.append(_ok(id_, name))
@ -308,7 +319,7 @@ def check_configurations(data):
if state is None:
results.append(_problem(id_, name, "error",
f"WireGuard interface {iface} does not exist.",
"Run sudo python3 core.py --apply to bring up WireGuard."))
"Run `sudo python3 core.py --apply` to bring up WireGuard."))
elif state in ("up", "unknown"): # WireGuard interfaces normally report 'unknown'
results.append(_ok(id_, name))
else:
@ -399,7 +410,7 @@ def check_configurations(data):
"radius_secret_match", "FreeRADIUS shared secret", "error",
"clients.conf secret does not match .radius-secret. "
"Access points will reject all authentication requests.",
"Restore .radius-secret from backup, or run sudo python3 core.py --apply "
"Restore .radius-secret from backup, or run `sudo python3 core.py --apply` "
"then update the shared secret in your AP controller."))
except OSError:
pass # already caught above by file_ok
@ -435,7 +446,7 @@ def check_configurations(data):
"avahi_ifaces", "avahi-daemon interface list", "warning",
f"avahi-daemon.conf interface list does not match config "
f"(missing: {missing or 'none'}, extra: {extra or 'none'}).",
"Run sudo python3 core.py --apply to update."))
"Run `sudo python3 core.py --apply` to update."))
else:
results.append(_ok("avahi_ifaces",
"avahi-daemon interface list"))
@ -458,11 +469,11 @@ def check_configurations(data):
"resolv_conf", "/etc/resolv.conf", "warning",
f"/etc/resolv.conf nameserver(s) {ns_ips} do not include any VLAN gateway. "
f"Expected one of: {gateway_ips}.",
"Run sudo python3 core.py --apply to update /etc/resolv.conf."))
"Run `sudo python3 core.py --apply` to update /etc/resolv.conf."))
except OSError:
results.append(_problem("resolv_conf", "/etc/resolv.conf", "warning",
"/etc/resolv.conf is not readable.",
"Run sudo python3 core.py --apply."))
"Run `sudo python3 core.py --apply`."))
# --- chrony.conf ---
if CHRONY_CONF_FILE.exists():
@ -482,7 +493,7 @@ def check_configurations(data):
results.append(_problem(
"chrony_conf", "/etc/chrony/chrony.conf", "warning",
f"chrony.conf is missing allow directives for: {', '.join(missing_subnets)}.",
"Run sudo python3 core.py --apply to update chrony.conf."))
"Run `sudo python3 core.py --apply` to update chrony.conf."))
else:
results.append(_ok("chrony_conf", "/etc/chrony/chrony.conf"))
except OSError:
@ -522,7 +533,7 @@ def check_configurations(data):
f"DHCP pool for VLAN '{vlan['name']}' is {pct}% full "
f"({len(leases)}/{pool_size} leases).",
"Expand the pool range in core.json or clean up stale leases "
"with: sudo python3 core.py --reset-leases " + vlan['name']))
f"with: `sudo python3 core.py --reset-leases {vlan['name']}`"))
else:
results.append(_ok(f"dhcp_pool_{vlan['name']}",
f"DHCP pool ({vlan['name']})",
@ -546,13 +557,13 @@ def check_configurations(data):
results.append(_problem(
f"blocklist_{h}", f"blocklist ({label})", "warning",
f"Merged blocklist file for '{label}' does not exist.",
"Run sudo python3 core.py --update-blocklists to download blocklists."))
"Run `sudo python3 core.py --update-blocklists` to download blocklists."))
elif now - path.stat().st_mtime > BLOCKLIST_STALE_SECS:
age_h = int((now - path.stat().st_mtime) / 3600)
results.append(_problem(
f"blocklist_{h}", f"blocklist ({label})", "warning",
f"Merged blocklist for '{label}' is {age_h}h old (threshold 36h).",
"Run sudo python3 core.py --update-blocklists to refresh."))
"Run `sudo python3 core.py --update-blocklists` to refresh."))
else:
results.append(_ok(f"blocklist_{h}", f"blocklist ({label})"))
@ -634,7 +645,7 @@ def check_logs(data):
"FreeRADIUS auth failures", "error",
f"FreeRADIUS is rejecting requests from {ap_str} with "
f"'Shared secret is incorrect' ({len(failures)} failures in the last hour).",
"Restore .radius-secret from backup and run sudo python3 core.py --apply, "
"Restore .radius-secret from backup and run `sudo python3 core.py --apply`, "
"or update the shared secret in your AP controller to match .radius-secret."))
else:
results.append(_ok("freeradius_auth_failures",
@ -668,7 +679,7 @@ def check_logs(data):
"dnsmasq_errors", "dnsmasq errors", "error",
f"{len(err_lines)} dnsmasq error(s) in the last hour: "
f"{err_lines[0][:120]}{'...' if len(err_lines) > 1 else ''}",
"Check dnsmasq logs: sudo journalctl -u 'dnsmasq-routlin-*' --since -1h"))
"Check dnsmasq logs: `sudo journalctl -u 'dnsmasq-routlin-*' --since -1h`"))
else:
results.append(_ok("dnsmasq_errors", "dnsmasq errors"))
except Exception:
@ -742,15 +753,21 @@ def print_table(status):
svc_problems = []
for svc in status.get("services", []):
if svc.get("status") == "problem":
parts = []
name = svc["name"]
utype = "timer" if name.endswith(".timer") else "service" if name.endswith(".service") else "unit"
exp_parts, act_parts, fix_parts = [], [], []
if not svc.get("active_ok"):
parts.append(f"active: {svc.get('active')} (expected {svc.get('expected_active')})")
exp_parts.append(svc.get("expected_active", "active"))
act_parts.append(svc.get("active", "unknown"))
fix_parts.append("activate")
if not svc.get("enabled_ok"):
parts.append(f"enabled: {svc.get('enabled')} (expected {svc.get('expected_enabled')})")
svc_problems.append({
"severity": "error",
"detail": f"{svc['name']}: {', '.join(parts)}",
})
exp_parts.append(svc.get("expected_enabled", "enabled"))
act_parts.append(svc.get("enabled", "unknown"))
fix_parts.append("enable")
detail = (f"The {utype} `{name}` is expected to be "
f"{' and '.join(exp_parts)} but is {' and '.join(act_parts)}.")
suggestion = f"Run `sudo python3 core.py --apply` to {' and '.join(reversed(fix_parts))} it."
svc_problems.append({"severity": svc.get("severity", "error"), "detail": detail, "suggestion": suggestion})
problems = svc_problems + [
item
for section in ("configurations", "logs")