Development
This commit is contained in:
parent
74166f03bd
commit
5dd309afc3
3 changed files with 90 additions and 76 deletions
|
|
@ -24,6 +24,7 @@ All configuration lives in two JSON files. Edit these to match your network befo
|
|||
| `.dashboard-last-run` | Epoch timestamp of the last timer execution. |
|
||||
| `.dashboard-lock` | PID lock file preventing concurrent timer runs. |
|
||||
| `.dashboard-pending` | Changes held back when Apply on Save is disabled; flushed to `.dashboard-queue` when Apply Now is clicked. |
|
||||
| `.status` | JSON health check results written by `core.py --apply`, `core.py --status`, and the dashboard timer. Read by the dashboard to display problem alerts. |
|
||||
| `.dns-metrics` | Cumulative lifetime DNS metrics across all VLAN instances. Created and updated each time `--view-metrics` is run. |
|
||||
| `.ddns-last-ip-*` | Cached public IP per DDNS provider. Managed by `ddns.py`. |
|
||||
| `.ddns-last-service` | Tracks IP-check service rotation. Managed by `ddns.py`. |
|
||||
|
|
|
|||
162
routlin/core.py
162
routlin/core.py
|
|
@ -120,11 +120,16 @@ SYSTEMD_DIR = Path("/etc/systemd/system")
|
|||
BLIST_TIMER_NAME = f"{PRODUCT_NAME}-dns-blocklist-update"
|
||||
BLIST_TIMER_FILE = SYSTEMD_DIR / f"{BLIST_TIMER_NAME}.timer"
|
||||
BLIST_TIMER_SVC_FILE = SYSTEMD_DIR / f"{BLIST_TIMER_NAME}.service"
|
||||
DASHB_TIMER_NAME = f"{PRODUCT_NAME}-dashboard-queue"
|
||||
DASHB_TIMER_FILE = SYSTEMD_DIR / f"{DASHB_TIMER_NAME}.timer"
|
||||
DASHB_TIMER_SVC_FILE = SYSTEMD_DIR / f"{DASHB_TIMER_NAME}.service"
|
||||
DASHB_TIMER_INTERVAL_SEC = 60
|
||||
DASHB_QUEUE_FILE = SCRIPT_DIR / ".dashboard-queue"
|
||||
DASHB_TIMER_NAME = f"{PRODUCT_NAME}-dashboard-queue"
|
||||
DASHB_TIMER_FILE = SYSTEMD_DIR / f"{DASHB_TIMER_NAME}.timer"
|
||||
DASHB_TIMER_SVC_FILE = SYSTEMD_DIR / f"{DASHB_TIMER_NAME}.service"
|
||||
DASHB_TIMER_INTERVAL_SEC = 60
|
||||
DASHB_QUEUE_FILE = SCRIPT_DIR / ".dashboard-queue"
|
||||
STATUS_TIMER_NAME = f"{PRODUCT_NAME}-status-check"
|
||||
STATUS_TIMER_FILE = SYSTEMD_DIR / f"{STATUS_TIMER_NAME}.timer"
|
||||
STATUS_TIMER_SVC_FILE = SYSTEMD_DIR / f"{STATUS_TIMER_NAME}.service"
|
||||
STATUS_TIMER_INTERVAL_SEC = 300
|
||||
STATUS_FILE = SCRIPT_DIR / ".status"
|
||||
DASHB_DONE_FILE = SCRIPT_DIR / ".dashboard-done"
|
||||
DASHB_LAST_RUN_FILE = SCRIPT_DIR / ".dashboard-last-run"
|
||||
DASHB_LOCK_FILE = SCRIPT_DIR / ".dashboard-lock"
|
||||
|
|
@ -1237,73 +1242,63 @@ def install_timer(data):
|
|||
capture_output=True, text=True)
|
||||
print(f"Timer {BLIST_TIMER_NAME}.timer enabled (runs daily at {execute_time}).")
|
||||
|
||||
def install_dashboard_timer():
|
||||
"""Install the 1-minute dashboard-queue timer that processes .dashboard-queue."""
|
||||
timer_content = "\n".join([
|
||||
"# Generated by core.py -- do not edit manually.",
|
||||
"",
|
||||
"[Unit]",
|
||||
"Description=Router dashboard pending-update processor",
|
||||
"",
|
||||
"[Timer]",
|
||||
f"OnActiveSec={DASHB_TIMER_INTERVAL_SEC}s",
|
||||
f"OnUnitActiveSec={DASHB_TIMER_INTERVAL_SEC}s",
|
||||
"AccuracySec=10s",
|
||||
"",
|
||||
"[Install]",
|
||||
"WantedBy=timers.target",
|
||||
"",
|
||||
])
|
||||
|
||||
service_content = "\n".join([
|
||||
"# Generated by core.py -- do not edit manually.",
|
||||
"",
|
||||
"[Unit]",
|
||||
"Description=Router dashboard update processor",
|
||||
"",
|
||||
"[Service]",
|
||||
"Type=oneshot",
|
||||
f"ExecStart=/bin/bash {DASHB_SCRIPT_FILE}",
|
||||
"",
|
||||
])
|
||||
|
||||
for path, content in ((DASHB_TIMER_FILE, timer_content), (DASHB_TIMER_SVC_FILE, service_content)):
|
||||
if not path.exists() or path.read_text() != content:
|
||||
path.write_text(content)
|
||||
print(f"Written: {path}")
|
||||
def _install_interval_timers(names, timer_files, svc_files, descriptions, exec_starts, interval_secs):
|
||||
for name, timer_file, svc_file, description, exec_start, interval_sec in zip(
|
||||
names, timer_files, svc_files, descriptions, exec_starts, interval_secs):
|
||||
timer_content = "\n".join([
|
||||
"# Generated by core.py -- do not edit manually.",
|
||||
"",
|
||||
"[Unit]",
|
||||
f"Description={description}",
|
||||
"",
|
||||
"[Timer]",
|
||||
f"OnActiveSec={interval_sec}s",
|
||||
f"OnUnitActiveSec={interval_sec}s",
|
||||
"AccuracySec=10s",
|
||||
"",
|
||||
"[Install]",
|
||||
"WantedBy=timers.target",
|
||||
"",
|
||||
])
|
||||
service_content = "\n".join([
|
||||
"# Generated by core.py -- do not edit manually.",
|
||||
"",
|
||||
"[Unit]",
|
||||
f"Description={description}",
|
||||
"",
|
||||
"[Service]",
|
||||
"Type=oneshot",
|
||||
f"ExecStart={exec_start}",
|
||||
"",
|
||||
])
|
||||
for path, content in ((timer_file, timer_content), (svc_file, service_content)):
|
||||
if not path.exists() or path.read_text() != content:
|
||||
path.write_text(content)
|
||||
print(f"Written: {path}")
|
||||
|
||||
subprocess.run(["systemctl", "daemon-reload"], capture_output=True, text=True)
|
||||
subprocess.run(["systemctl", "enable", f"{DASHB_TIMER_NAME}.timer"],
|
||||
capture_output=True, text=True)
|
||||
active = subprocess.run(
|
||||
["systemctl", "is-active", f"{DASHB_TIMER_NAME}.timer"],
|
||||
capture_output=True, text=True
|
||||
).stdout.strip() == "active"
|
||||
verb = "restart" if active else "start"
|
||||
subprocess.run(["systemctl", verb, f"{DASHB_TIMER_NAME}.timer"],
|
||||
capture_output=True, text=True)
|
||||
print(f"Timer {DASHB_TIMER_NAME}.timer enabled (runs every {DASHB_TIMER_INTERVAL_SEC}s).")
|
||||
for name, interval_sec in zip(names, interval_secs):
|
||||
subprocess.run(["systemctl", "enable", f"{name}.timer"], capture_output=True, text=True)
|
||||
active = subprocess.run(
|
||||
["systemctl", "is-active", f"{name}.timer"],
|
||||
capture_output=True, text=True
|
||||
).stdout.strip() == "active"
|
||||
verb = "restart" if active else "start"
|
||||
subprocess.run(["systemctl", verb, f"{name}.timer"], capture_output=True, text=True)
|
||||
print(f"Timer {name}.timer enabled (runs every {interval_sec}s).")
|
||||
|
||||
def remove_dashboard_timer():
|
||||
subprocess.run(["systemctl", "disable", "--now", f"{DASHB_TIMER_NAME}.timer"],
|
||||
capture_output=True, text=True)
|
||||
for f in (DASHB_TIMER_FILE, DASHB_TIMER_SVC_FILE):
|
||||
if f.exists():
|
||||
f.unlink()
|
||||
print(f"Removed: {f}")
|
||||
else:
|
||||
print(f"Not found, skipping: {f}")
|
||||
|
||||
def remove_timer():
|
||||
subprocess.run(["systemctl", "disable", "--now", f"{BLIST_TIMER_NAME}.timer"],
|
||||
capture_output=True, text=True)
|
||||
for f in (BLIST_TIMER_FILE, BLIST_TIMER_SVC_FILE):
|
||||
if f.exists():
|
||||
f.unlink()
|
||||
print(f"Removed: {f}")
|
||||
else:
|
||||
print(f"Not found, skipping: {f}")
|
||||
subprocess.run(["systemctl", "daemon-reload"], capture_output=True, text=True)
|
||||
def _remove_timers(names, timer_files, svc_files, daemon_reload=False):
|
||||
for name, timer_file, svc_file in zip(names, timer_files, svc_files):
|
||||
subprocess.run(["systemctl", "disable", "--now", f"{name}.timer"],
|
||||
capture_output=True, text=True)
|
||||
for f in (timer_file, svc_file):
|
||||
if f.exists():
|
||||
f.unlink()
|
||||
print(f"Removed: {f}")
|
||||
else:
|
||||
print(f"Not found, skipping: {f}")
|
||||
if daemon_reload:
|
||||
subprocess.run(["systemctl", "daemon-reload"], capture_output=True, text=True)
|
||||
|
||||
# ===================================================================
|
||||
# banned_ips expansion
|
||||
|
|
@ -2487,9 +2482,13 @@ def show_metrics(data):
|
|||
# ===================================================================
|
||||
|
||||
def stop_instances(data):
|
||||
"""Remove timer and stop all per-VLAN instances (config files preserved)."""
|
||||
remove_timer()
|
||||
remove_dashboard_timer()
|
||||
"""Remove timers and stop all per-VLAN instances (config files preserved)."""
|
||||
_remove_timers(
|
||||
names=[BLIST_TIMER_NAME, DASHB_TIMER_NAME, STATUS_TIMER_NAME],
|
||||
timer_files=[BLIST_TIMER_FILE, DASHB_TIMER_FILE, STATUS_TIMER_FILE],
|
||||
svc_files=[BLIST_TIMER_SVC_FILE, DASHB_TIMER_SVC_FILE, STATUS_TIMER_SVC_FILE],
|
||||
daemon_reload=True,
|
||||
)
|
||||
print()
|
||||
for vlan in data["vlans"]:
|
||||
svc = vlan_service_name(vlan, derive_interface(vlan, data))
|
||||
|
|
@ -3080,10 +3079,23 @@ def cmd_apply(data, dry_run=False):
|
|||
install_timer(data)
|
||||
print()
|
||||
|
||||
print("Interval timers =====================================================")
|
||||
# build parallel lists; dashboard timer only installed when queue file exists
|
||||
t_names = [STATUS_TIMER_NAME]
|
||||
t_files = [STATUS_TIMER_FILE]
|
||||
s_files = [STATUS_TIMER_SVC_FILE]
|
||||
t_descs = ["Router status health check"]
|
||||
t_execs = [f"/usr/bin/python3 {SCRIPT_DIR / 'status.py'}"]
|
||||
t_intervals = [STATUS_TIMER_INTERVAL_SEC]
|
||||
if DASHB_QUEUE_FILE.exists():
|
||||
print("Dashboard timer =====================================================")
|
||||
install_dashboard_timer()
|
||||
print()
|
||||
t_names += [DASHB_TIMER_NAME]
|
||||
t_files += [DASHB_TIMER_FILE]
|
||||
s_files += [DASHB_TIMER_SVC_FILE]
|
||||
t_descs += ["Router dashboard pending-update processor"]
|
||||
t_execs += [f"/bin/bash {DASHB_SCRIPT_FILE}"]
|
||||
t_intervals += [DASHB_TIMER_INTERVAL_SEC]
|
||||
_install_interval_timers(t_names, t_files, s_files, t_descs, t_execs, t_intervals)
|
||||
print()
|
||||
|
||||
print("Boot service ========================================================")
|
||||
install_nat_service()
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ DASHB_DONE_FILE = SCRIPT_DIR / ".dashboard-done"
|
|||
DASHB_LAST_RUN_FILE = SCRIPT_DIR / ".dashboard-last-run"
|
||||
DASHB_LOCK_FILE = SCRIPT_DIR / ".dashboard-lock"
|
||||
DASHB_PENDING_FILE = SCRIPT_DIR / ".dashboard-pending"
|
||||
STATUS_FILE = SCRIPT_DIR / ".status"
|
||||
|
||||
|
||||
# ===================================================================
|
||||
|
|
@ -304,7 +305,7 @@ def setup_docker_compose():
|
|||
|
||||
|
||||
def create_dotfiles():
|
||||
for f in (DASHB_QUEUE_FILE, DASHB_DONE_FILE, DASHB_LAST_RUN_FILE, DASHB_LOCK_FILE, DASHB_PENDING_FILE):
|
||||
for f in (DASHB_QUEUE_FILE, DASHB_DONE_FILE, DASHB_LAST_RUN_FILE, DASHB_LOCK_FILE, DASHB_PENDING_FILE, STATUS_FILE):
|
||||
if not f.exists():
|
||||
f.touch()
|
||||
# chown to the routlin dir owner so the timer can write
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue