Development
This commit is contained in:
parent
8766c6c9a2
commit
ee31a18ac6
43 changed files with 54 additions and 48 deletions
205
docker/routlin-dash/app/config_utils.py
Normal file
205
docker/routlin-dash/app/config_utils.py
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import json, subprocess, hashlib, os, uuid
|
||||
from datetime import datetime, timezone
|
||||
from flask import session
|
||||
|
||||
CONFIGS_DIR = '/configs'
|
||||
CORE_FILE = f'{CONFIGS_DIR}/core.json'
|
||||
DASHBOARD_QUEUE = f'{CONFIGS_DIR}/.dashboard-queue'
|
||||
DASHBOARD_DONE = f'{CONFIGS_DIR}/.dashboard-done'
|
||||
DASHBOARD_LAST_RUN = f'{CONFIGS_DIR}/.dashboard-last-run'
|
||||
DASHBOARD_LOCK = f'{CONFIGS_DIR}/.dashboard-lock'
|
||||
DASHB_TIMER_NAME = 'routlin-dashboard-queue'
|
||||
PRODUCT_DISPLAY_NAME = os.environ.get('PRODUCT_DISPLAY_NAME', 'Routlin Dashboard')
|
||||
DASHB_INTERVAL_SECS = 60
|
||||
QUEUE_MAX_LINES = 50
|
||||
|
||||
|
||||
def load_core():
|
||||
try:
|
||||
with open(CORE_FILE) as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def save_core(data):
|
||||
with open(CORE_FILE, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
def core_hash():
|
||||
try:
|
||||
with open(CORE_FILE, 'rb') as f:
|
||||
return hashlib.md5(f.read()).hexdigest()
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
|
||||
def verify_core_hash(submitted):
|
||||
if not submitted:
|
||||
return True
|
||||
return submitted == core_hash()
|
||||
|
||||
|
||||
def _load_done_set():
|
||||
try:
|
||||
done = set()
|
||||
for line in open(DASHBOARD_DONE).read().splitlines():
|
||||
parts = line.split()
|
||||
if parts:
|
||||
done.add(parts[0])
|
||||
return done
|
||||
except Exception:
|
||||
return set()
|
||||
|
||||
|
||||
def _read_pending(done_set):
|
||||
pending = []
|
||||
try:
|
||||
lines = open(DASHBOARD_QUEUE).read().splitlines()
|
||||
except Exception:
|
||||
return pending
|
||||
for line in lines:
|
||||
try:
|
||||
parts = line.split(None, 3)
|
||||
if len(parts) == 4:
|
||||
entry_uuid, entry_ts, _dt, rest = parts
|
||||
cmd_user = rest.rsplit(' (', 1)
|
||||
entry_cmd = cmd_user[0].strip('[]')
|
||||
entry_user = cmd_user[1].rstrip(')') if len(cmd_user) == 2 else ''
|
||||
if entry_uuid not in done_set:
|
||||
pending.append((entry_uuid, int(entry_ts), entry_cmd, entry_user))
|
||||
except Exception:
|
||||
pass
|
||||
return pending
|
||||
|
||||
|
||||
def get_pending_entries():
|
||||
return _read_pending(_load_done_set())
|
||||
|
||||
|
||||
def _format_timing(secs):
|
||||
if secs is None:
|
||||
return None
|
||||
if secs <= 5:
|
||||
return 'momentarily'
|
||||
if secs < 60:
|
||||
return f'in about {secs} seconds'
|
||||
mins = round(secs / 60)
|
||||
return f'in about {mins} minute{"s" if mins != 1 else ""}'
|
||||
|
||||
|
||||
def _trim_if_needed():
|
||||
try:
|
||||
lines = [l for l in open(DASHBOARD_QUEUE).read().splitlines() if l]
|
||||
if len(lines) <= QUEUE_MAX_LINES:
|
||||
return
|
||||
done_set = _load_done_set()
|
||||
pending = [l for l in lines if l.split()[0] not in done_set]
|
||||
with open(DASHBOARD_QUEUE, 'w') as f:
|
||||
f.write('\n'.join(pending) + ('\n' if pending else ''))
|
||||
open(DASHBOARD_DONE, 'w').close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _queue_command(cmd):
|
||||
done_set = _load_done_set()
|
||||
pending = _read_pending(done_set)
|
||||
current_user = session.get('email_address', 'unknown')
|
||||
for entry_uuid, entry_ts, entry_cmd, entry_user in pending:
|
||||
if entry_cmd == cmd and entry_user == current_user:
|
||||
return entry_uuid, entry_ts
|
||||
entry_uuid = str(uuid.uuid4())
|
||||
now = datetime.now()
|
||||
entry_ts = int(now.timestamp())
|
||||
dt_str = now.strftime('%Y-%m-%dT%H:%M:%S')
|
||||
user = session.get('email_address', 'unknown')
|
||||
with open(DASHBOARD_QUEUE, 'a') as f:
|
||||
f.write(f'{entry_uuid} {entry_ts} {dt_str} [{cmd}] ({user})\n')
|
||||
_trim_if_needed()
|
||||
return entry_uuid, entry_ts
|
||||
|
||||
|
||||
def _entry_ts_from_queue(entry_uuid):
|
||||
try:
|
||||
for line in open(DASHBOARD_QUEUE).read().splitlines():
|
||||
parts = line.split(None, 2)
|
||||
if len(parts) >= 2 and parts[0] == entry_uuid:
|
||||
return int(parts[1])
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _seconds_until_next_run():
|
||||
try:
|
||||
last_run = float(open(DASHBOARD_LAST_RUN).read().strip())
|
||||
elapsed = datetime.now(timezone.utc).timestamp() - last_run
|
||||
return int(max(0, DASHB_INTERVAL_SECS - elapsed))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _is_locked():
|
||||
try:
|
||||
return os.path.getsize(DASHBOARD_LOCK) > 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _lock_mtime():
|
||||
try:
|
||||
return os.path.getmtime(DASHBOARD_LOCK)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def queue_command(cmd):
|
||||
"""Queue a command without generating a flash message."""
|
||||
return _queue_command(cmd)
|
||||
|
||||
|
||||
def queued_msg(cmd=None):
|
||||
"""Queue cmd if given, then return a timing message.
|
||||
Without cmd, just returns timing (for commands already queued by the caller)."""
|
||||
entry_ts = None
|
||||
if cmd is not None:
|
||||
_entry_uuid, entry_ts = queue_command(cmd)
|
||||
if _is_locked():
|
||||
mtime = _lock_mtime()
|
||||
if entry_ts is not None and mtime and entry_ts < mtime:
|
||||
return 'Configuration saved. Changes are being applied now.'
|
||||
return 'Configuration saved. Changes will be applied on the next run.'
|
||||
timing = _format_timing(_seconds_until_next_run())
|
||||
if timing:
|
||||
return f'Configuration saved. Changes will be applied {timing}.'
|
||||
if cmd is None:
|
||||
return 'Changes queued. The processing service is not running.'
|
||||
parts = cmd.split()
|
||||
cli_cmd = f'sudo python3 {parts[0]}.py --{parts[1]}' if len(parts) == 2 else cmd
|
||||
install_cmd = f'sudo python3 {parts[0]}.py --install' if len(parts) >= 1 else 'core.py --install'
|
||||
from markupsafe import Markup
|
||||
return Markup(f'Configuration saved. The command processing service is not installed. '
|
||||
f'Run <strong>{install_cmd}</strong> to enable it, '
|
||||
f'or <strong>{cli_cmd}</strong> to apply manually.')
|
||||
|
||||
|
||||
def run_apply():
|
||||
try:
|
||||
subprocess.run(
|
||||
['python3', f'{CONFIGS_DIR}/core.py', '--apply'],
|
||||
capture_output=True, timeout=30
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def run_update_blocklists():
|
||||
try:
|
||||
subprocess.run(
|
||||
['python3', f'{CONFIGS_DIR}/core.py', '--update-blocklists'],
|
||||
capture_output=True, timeout=120
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
Loading…
Add table
Add a link
Reference in a new issue