Development
This commit is contained in:
parent
e72676eac5
commit
80390334fb
2 changed files with 75 additions and 45 deletions
|
|
@ -1495,12 +1495,25 @@ def render_layout(view_id, content_html, tokens):
|
|||
grouped.setdefault(sev, []).append(text)
|
||||
for item in st.get('services', []):
|
||||
if item.get('status') == 'problem':
|
||||
parts = []
|
||||
name = item.get('name', '')
|
||||
utype = 'timer' if name.endswith('.timer') else 'service' if name.endswith('.service') else 'unit'
|
||||
exp_parts, act_parts, fix_parts = [], [], []
|
||||
if not item.get('active_ok'):
|
||||
parts.append(f"active: {item.get('active')} (expected {item.get('expected_active')})")
|
||||
exp_parts.append(item.get('expected_active', 'active'))
|
||||
act_parts.append(item.get('active', 'unknown'))
|
||||
fix_parts.append('activate')
|
||||
if not item.get('enabled_ok'):
|
||||
parts.append(f"enabled: {item.get('enabled')} (expected {item.get('expected_enabled')})")
|
||||
grouped['error'].append(e(f"{item.get('name')}: {', '.join(parts)}"))
|
||||
exp_parts.append(item.get('expected_enabled', 'enabled'))
|
||||
act_parts.append(item.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)}.")
|
||||
tip = f"Run `sudo python3 core.py --apply` to {' and '.join(reversed(fix_parts))} it."
|
||||
sev = item.get('severity', 'error')
|
||||
text = e(detail)
|
||||
if tip:
|
||||
text += f' <span style="opacity:0.75">{e(tip)}</span>'
|
||||
grouped.setdefault(sev, []).append(text)
|
||||
for sev, items in grouped.items():
|
||||
if not items:
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue