Development
This commit is contained in:
parent
59ac3c5973
commit
3d0dc265ba
31 changed files with 1093 additions and 2794 deletions
|
|
@ -16,6 +16,7 @@ DASHBOARD_LOCK = f'{CONFIGS_DIR}/.dashboard-lock'
|
|||
DASHBOARD_PENDING = f'{CONFIGS_DIR}/.dashboard-pending'
|
||||
DASHBOARD_DB = f'{CONFIGS_DIR}/.dashboard-snapshots'
|
||||
HEALTH_FILE = f'{CONFIGS_DIR}/.health'
|
||||
BLOCKLISTS_DIR = f'{CONFIGS_DIR}/blocklists'
|
||||
PRODUCT_NAME = os.environ.get('PRODUCT_NAME', 'routlin')
|
||||
DASHB_TIMER_NAME = f'{PRODUCT_NAME}-dashboard-queue'
|
||||
DDNS_TIMER_NAME = f'{PRODUCT_NAME}-ddns-update'
|
||||
|
|
@ -578,3 +579,229 @@ def run_update_blocklists():
|
|||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# Format helpers ====================================================
|
||||
|
||||
def fmt_timestamp(ts):
|
||||
try:
|
||||
return datetime.fromtimestamp(ts, tz=timezone.utc).strftime('%Y-%m-%d %H:%M UTC')
|
||||
except Exception:
|
||||
return '-'
|
||||
|
||||
def relative_time(ts1, ts2, short=False):
|
||||
try:
|
||||
diff = abs(int(ts1) - int(ts2))
|
||||
if diff < 60:
|
||||
return f'{diff}s' if short else f'{diff} second{"s" if diff != 1 else ""}'
|
||||
m = diff // 60
|
||||
if m < 60:
|
||||
return f'{m}m' if short else f'{m} minute{"s" if m != 1 else ""}'
|
||||
h, rem_m = divmod(m, 60)
|
||||
if h < 24:
|
||||
if short:
|
||||
return f'{h}h {rem_m}m' if rem_m else f'{h}h'
|
||||
return f'{h}h {rem_m}m' if rem_m else f'{h} hour{"s" if h != 1 else ""}'
|
||||
d = h // 24
|
||||
if d < 365:
|
||||
return f'{d}d' if short else f'{d} day{"s" if d != 1 else ""}'
|
||||
y = d // 365
|
||||
return f'{y}y' if short else f'{y} year{"s" if y != 1 else ""}'
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
def fmt_bytes(n):
|
||||
for unit in ('B', 'KB', 'MB', 'GB'):
|
||||
if n < 1024:
|
||||
return f'{n:.1f} {unit}'
|
||||
n /= 1024
|
||||
return f'{n:.1f} TB'
|
||||
|
||||
def resolve_iface(vlan, cfg):
|
||||
if vlan.get('is_vpn'):
|
||||
wg_vlans = [v for v in cfg.get('vlans', []) if v.get('is_vpn')]
|
||||
wg_sorted = sorted(wg_vlans, key=lambda v: (v.get('vlan_id') is None, v.get('vlan_id') or 0))
|
||||
idx = next((i for i, v in enumerate(wg_sorted) if v is vlan), 0)
|
||||
return f'wg{idx}'
|
||||
lan = cfg.get('network_interfaces', {}).get('lan_interface', 'eth0')
|
||||
vid = vlan.get('vlan_id') or 1
|
||||
return lan if vid == 1 else f'{lan}.{vid}'
|
||||
|
||||
|
||||
# Config datasources ================================================
|
||||
|
||||
def config_datasource(name):
|
||||
cfg = load_config()
|
||||
vlans = cfg.get('vlans', [])
|
||||
|
||||
if name == 'banned_ips':
|
||||
return cfg.get('banned_ips', [])
|
||||
|
||||
if name == 'host_overrides':
|
||||
return cfg.get('host_overrides', [])
|
||||
|
||||
if name == 'blocklists':
|
||||
rows = []
|
||||
for bl in cfg.get('dns_blocking', {}).get('blocklists', []):
|
||||
row = dict(bl)
|
||||
bl_path = os.path.join(BLOCKLISTS_DIR, bl.get('save_as', ''))
|
||||
try:
|
||||
with open(bl_path) as f:
|
||||
row['domain_count'] = str(sum(1 for _ in f))
|
||||
row['last_updated'] = fmt_timestamp(int(os.path.getmtime(bl_path)))
|
||||
except Exception:
|
||||
row['domain_count'] = '-'
|
||||
row['last_updated'] = '-'
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
if name == 'vlans':
|
||||
bl_desc = {
|
||||
b['name']: b.get('description', b['name'])
|
||||
for b in cfg.get('dns_blocking', {}).get('blocklists', [])
|
||||
if 'name' in b
|
||||
}
|
||||
rows = []
|
||||
for v in sorted(vlans, key=lambda x: x.get('vlan_id') or 0):
|
||||
row = {k: v.get(k) for k in (
|
||||
'name', 'subnet', 'subnet_mask', 'radius_default',
|
||||
'mdns_reflection', 'is_vpn', 'dnsmasq_log_queries'
|
||||
)}
|
||||
row['vlan_id'] = v.get('vlan_id')
|
||||
row['interface'] = resolve_iface(v, cfg)
|
||||
row['use_blocklists'] = json.dumps([
|
||||
{'n': bl, 'd': bl_desc.get(bl, bl)} for bl in v.get('use_blocklists', [])
|
||||
])
|
||||
prefix = v.get('subnet_mask', 24)
|
||||
n_octets = 1 if prefix >= 24 else 2 if prefix >= 16 else 3 if prefix >= 8 else 4
|
||||
row['server_identity_ips'] = json.dumps([
|
||||
{
|
||||
'n': s['ip'],
|
||||
'd': ' | '.join(filter(None, [s['ip'], s.get('description'), s.get('hostname')])),
|
||||
'short': '.' + '.'.join(s['ip'].split('.')[-n_octets:]),
|
||||
'mini': '.' + '.'.join(s['ip'].split('.')[-n_octets:]),
|
||||
}
|
||||
for s in v.get('server_identities', []) if s.get('ip')
|
||||
])
|
||||
row['server_identity_descriptions'] = json.dumps([
|
||||
s.get('description', '') for s in v.get('server_identities', []) if s.get('ip')
|
||||
])
|
||||
row['server_identity_hostnames'] = json.dumps([
|
||||
s.get('hostname', '') for s in v.get('server_identities', []) if s.get('ip')
|
||||
])
|
||||
row['server_identity_gateway'] = (
|
||||
v.get('dhcp_information', {}).get('explicit_overrides', {}).get('gateway', '')
|
||||
)
|
||||
dns = v.get('dhcp_information', {}).get('explicit_overrides', {}).get('dns_servers', [])
|
||||
row['server_identity_dns_servers'] = '\n'.join(dns) if isinstance(dns, list) else str(dns or '')
|
||||
ntp = v.get('dhcp_information', {}).get('explicit_overrides', {}).get('ntp_servers', [])
|
||||
row['server_identity_ntp_servers'] = '\n'.join(ntp) if isinstance(ntp, list) else str(ntp or '')
|
||||
row['gateway'] = row['server_identity_gateway']
|
||||
row['dns_servers'] = row['server_identity_dns_servers']
|
||||
row['ntp_servers'] = row['server_identity_ntp_servers']
|
||||
row['dns_servers_override'] = 1 if row['server_identity_dns_servers'] else 0
|
||||
row['ntp_servers_override'] = 1 if row['server_identity_ntp_servers'] else 0
|
||||
dhi = v.get('dhcp_information', {})
|
||||
row['dhcp_pool_start'] = dhi.get('dynamic_pool_start', '')
|
||||
row['dhcp_pool_end'] = dhi.get('dynamic_pool_end', '')
|
||||
lt = dhi.get('lease_time', '')
|
||||
if lt and len(lt) > 1 and lt[:-1].isdigit() and lt[-1] in 'mhd':
|
||||
row['dhcp_lease_time'] = lt[:-1]
|
||||
row['dhcp_lease_unit'] = {'m': 'minutes', 'h': 'hours', 'd': 'days'}[lt[-1]]
|
||||
else:
|
||||
row['dhcp_lease_time'] = ''
|
||||
row['dhcp_lease_unit'] = ''
|
||||
row['dhcp_domain'] = dhi.get('domain', '')
|
||||
row['server_identities_json'] = json.dumps(v.get('server_identities', []))
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
if name == 'inter_vlan_exceptions':
|
||||
return cfg.get('inter_vlan_exceptions', [])
|
||||
|
||||
if name == 'port_forwarding':
|
||||
return cfg.get('port_forwarding', [])
|
||||
|
||||
if name == 'port_wrangling':
|
||||
rows = []
|
||||
for r in cfg.get('port_wrangling', []):
|
||||
row = dict(r)
|
||||
row['vlan_name'] = r.get('vlan', '-')
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
if name == 'dhcp_reservations':
|
||||
rows = []
|
||||
for res in cfg.get('dhcp_reservations', []):
|
||||
row = dict(res)
|
||||
row['vlan_name'] = res.get('vlan', '-')
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
if name == 'ddns_providers':
|
||||
from factory import e
|
||||
ddns = load_config().get('ddns', {})
|
||||
rows = []
|
||||
for p in ddns.get('providers', []):
|
||||
row = dict(p)
|
||||
ptype = p.get('provider', '').lower()
|
||||
if ptype == 'noip':
|
||||
row['credentials'] = (
|
||||
'<div style="line-height:1.3">'
|
||||
f'<b>U:</b> {e(p.get("username", "-"))}<br/>'
|
||||
'<b>P:</b> ••••••</div>'
|
||||
)
|
||||
elif ptype in ('cloudflare', 'duckdns'):
|
||||
tok = p.get('api_token', '')
|
||||
row['credentials'] = f'<b>API Token:</b> {e(tok[:20])}...' if tok else '(not set)'
|
||||
else:
|
||||
row['credentials'] = '-'
|
||||
row['hostnames'] = json.dumps(p.get('hostnames', p.get('subdomains', [])))
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
if name == 'accounts':
|
||||
try:
|
||||
with open(ACCOUNTS_FILE) as f:
|
||||
data = json.load(f)
|
||||
except Exception:
|
||||
data = {}
|
||||
rows = []
|
||||
for acct in data.get('accounts', []):
|
||||
row = dict(acct)
|
||||
row['account_status'] = 'active' if acct.get('hashed_password') else 'pending'
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
if name == 'vpn_peers':
|
||||
rows = []
|
||||
wg_sorted = sorted(
|
||||
[v for v in vlans if v.get('is_vpn')],
|
||||
key=lambda v: (v.get('vlan_id') is None, v.get('vlan_id') or 0)
|
||||
)
|
||||
for i, vlan in enumerate(wg_sorted):
|
||||
iface = f'wg{i}'
|
||||
vlan_display = f'{iface} (VLAN {vlan.get("vlan_id") or "?"})'
|
||||
for peer in vlan.get('peers', []):
|
||||
row = dict(peer)
|
||||
row['vlan_display'] = vlan_display
|
||||
row['split_tunnel'] = 'yes' if peer.get('split_tunnel') else 'no'
|
||||
row['pubkey_short'] = peer.get('public_key', '')[:20] + '...' if peer.get('public_key') else '-'
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def load_datasource(spec):
|
||||
if spec.startswith('config:'):
|
||||
return config_datasource(spec[7:])
|
||||
return []
|
||||
|
||||
|
||||
def collect_layout_tokens(cfg):
|
||||
net = cfg.get('network_interfaces', {})
|
||||
return {
|
||||
'GENERAL_LAN_INTERFACE': str(net.get('lan_interface', '-')),
|
||||
'VPN_VLAN_COUNT': str(sum(1 for v in cfg.get('vlans', []) if v.get('is_vpn'))),
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue