Development
This commit is contained in:
parent
9f1b4a9119
commit
8eec61a1df
8 changed files with 697 additions and 56 deletions
|
|
@ -48,9 +48,18 @@
|
|||
"label": "VLAN",
|
||||
"field": "vlan_name"
|
||||
},
|
||||
{
|
||||
"label": "Obtained",
|
||||
"field": "obtained"
|
||||
},
|
||||
{
|
||||
"label": "Expires",
|
||||
"field": "expires"
|
||||
},
|
||||
{
|
||||
"label": "Recent",
|
||||
"field": "recent",
|
||||
"render": "badge_yes_no"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,12 +45,12 @@ def options_save():
|
|||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
before = copy.deepcopy(cfg.get('free_radius', {}).get('options', {}))
|
||||
before = copy.deepcopy(cfg.get('radius', {}).get('options', {}))
|
||||
after = {'mac_format': mac_format, 'apply_to': apply_to}
|
||||
cfg.setdefault('free_radius', {})['options'] = after
|
||||
cfg.setdefault('radius', {})['options'] = after
|
||||
|
||||
changes = diff_fields(before, after)
|
||||
flash(record_group(cfg, 'free_radius.options', 'setting', 'free_radius', changes, 'core apply'), 'success')
|
||||
flash(record_group(cfg, 'radius.options', 'setting', 'radius', changes, 'core apply'), 'success')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
|
|
@ -64,12 +64,12 @@ def logging_save():
|
|||
logging = 'logging' in request.form
|
||||
|
||||
cfg = load_config()
|
||||
before = copy.deepcopy(cfg.get('free_radius', {}).get('general', {}))
|
||||
before = copy.deepcopy(cfg.get('radius', {}).get('general', {}))
|
||||
after = {'logging': logging, 'log_max_kb': log_max_kb}
|
||||
cfg.setdefault('free_radius', {})['general'] = after
|
||||
cfg.setdefault('radius', {})['general'] = after
|
||||
|
||||
changes = diff_fields(before, after)
|
||||
flash(record_group(cfg, 'free_radius.general', 'setting', 'free_radius', changes, 'core apply'), 'success')
|
||||
flash(record_group(cfg, 'radius.general', 'setting', 'radius', changes, 'core apply'), 'success')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ def logging_download():
|
|||
def api_log_tail():
|
||||
try:
|
||||
cfg = load_config()
|
||||
log_max_kb = cfg.get('free_radius', {}).get('general', {}).get('log_max_kb', 1024)
|
||||
log_max_kb = cfg.get('radius', {}).get('general', {}).get('log_max_kb', 1024)
|
||||
size_kb = os.path.getsize(RADIUS_LOG_FILE) / 1024
|
||||
with open(RADIUS_LOG_FILE) as f:
|
||||
lines = f.readlines()
|
||||
|
|
|
|||
|
|
@ -128,6 +128,22 @@
|
|||
"type": "raw_html",
|
||||
"html": "%RADIUS_LOG_SUMMARY%"
|
||||
},
|
||||
{
|
||||
"type": "button_row",
|
||||
"justify": "space-between",
|
||||
"items": [
|
||||
{
|
||||
"type": "button_ghost",
|
||||
"action": "/action/radius/logging_download",
|
||||
"text": "Download Log"
|
||||
},
|
||||
{
|
||||
"type": "button_danger",
|
||||
"formaction": "/action/radius/logging_clear",
|
||||
"text": "Clear Log"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "hr"
|
||||
},
|
||||
|
|
@ -155,23 +171,6 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "button_row",
|
||||
"justify": "space-between",
|
||||
"items": [
|
||||
{
|
||||
"type": "button_ghost",
|
||||
"action": "/action/radius/logging_download",
|
||||
"text": "Download Log"
|
||||
},
|
||||
{
|
||||
"type": "button_danger",
|
||||
"action": "/action/radius/logging_clear",
|
||||
"method": "post",
|
||||
"text": "Clear Log"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,21 +151,64 @@ def resolve_iface(vlan, cfg):
|
|||
|
||||
# Live data loaders =================================================
|
||||
|
||||
def _parse_lease_secs(s):
|
||||
s = str(s).strip().lower()
|
||||
try:
|
||||
if s.endswith('h'): return int(s[:-1]) * 3600
|
||||
if s.endswith('m'): return int(s[:-1]) * 60
|
||||
if s.endswith('d'): return int(s[:-1]) * 86400
|
||||
except ValueError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _dnsmasq_start_time(vlan_name):
|
||||
"""Return epoch timestamp when the dnsmasq instance for this VLAN last started."""
|
||||
try:
|
||||
pid = int(open(f'/run/dnsmasq-routlin-{vlan_name}.pid').read().strip())
|
||||
start_ticks = int(open(f'/proc/{pid}/stat').read().split()[21])
|
||||
clk_tck = os.sysconf('SC_CLK_TCK')
|
||||
boot_time = next(
|
||||
int(line.split()[1]) for line in open('/proc/stat') if line.startswith('btime ')
|
||||
)
|
||||
return boot_time + start_ticks / clk_tck
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def live_dhcp_leases():
|
||||
rows = []
|
||||
for leases_file in glob.glob('/var/lib/misc/*.leases'):
|
||||
now = int(datetime.now(tz=timezone.utc).timestamp())
|
||||
vlans = load_config().get('vlans', [])
|
||||
vlan_lease_secs = {
|
||||
v['name']: _parse_lease_secs(v.get('dhcp', {}).get('lease_time', ''))
|
||||
for v in vlans if v.get('name')
|
||||
}
|
||||
for leases_file in glob.glob('/var/lib/misc/dnsmasq-routlin-*.leases'):
|
||||
stem = os.path.basename(leases_file)
|
||||
vlan_name = stem[len('dnsmasq-routlin-'):-len('.leases')]
|
||||
lease_secs = vlan_lease_secs.get(vlan_name)
|
||||
restart_time = _dnsmasq_start_time(vlan_name)
|
||||
try:
|
||||
with open(leases_file) as f:
|
||||
for line in f:
|
||||
parts = line.strip().split()
|
||||
if len(parts) >= 4:
|
||||
rows.append({
|
||||
'hostname': parts[3] if parts[3] != '*' else '-',
|
||||
'ip_address': parts[2],
|
||||
'mac_address': parts[1],
|
||||
'vlan_name': _vlan_name_for_ip(parts[2]),
|
||||
'expires': fmt_timestamp(int(parts[0])),
|
||||
})
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
expiry = int(parts[0])
|
||||
if expiry < now:
|
||||
continue
|
||||
obtained_ts = (expiry - lease_secs) if lease_secs else None
|
||||
obtained = relative_time(obtained_ts) if obtained_ts else '-'
|
||||
recent = (obtained_ts is not None and restart_time is not None
|
||||
and obtained_ts >= restart_time)
|
||||
rows.append({
|
||||
'hostname': parts[3] if parts[3] != '*' else '-',
|
||||
'ip_address': parts[2],
|
||||
'mac_address': parts[1],
|
||||
'vlan_name': vlan_name,
|
||||
'obtained': obtained,
|
||||
'expires': relative_time_future(expiry),
|
||||
'recent': recent,
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return rows
|
||||
|
|
@ -210,6 +253,24 @@ def relative_time(ts):
|
|||
except Exception:
|
||||
return ''
|
||||
|
||||
def relative_time_future(ts):
|
||||
try:
|
||||
diff = int(ts) - int(datetime.now(tz=timezone.utc).timestamp())
|
||||
if diff <= 0:
|
||||
return 'expired'
|
||||
if diff < 60:
|
||||
return f'in {diff} second{"s" if diff != 1 else ""}'
|
||||
m = diff // 60
|
||||
if m < 60:
|
||||
return f'in {m} minute{"s" if m != 1 else ""}'
|
||||
h, rem_m = divmod(m, 60)
|
||||
if h < 24:
|
||||
return f'in {h}h {rem_m}m' if rem_m else f'in {h} hour{"s" if h != 1 else ""}'
|
||||
d = h // 24
|
||||
return f'in {d} day{"s" if d != 1 else ""}'
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
def live_vpn_sessions():
|
||||
rows = []
|
||||
out = run('wg show all dump 2>/dev/null')
|
||||
|
|
@ -509,7 +570,7 @@ RADIUS_LOG_FILE = '/var/log/freeradius/radius.log'
|
|||
def _radius_log_tail():
|
||||
try:
|
||||
cfg = load_config()
|
||||
log_max_kb = cfg.get('free_radius', {}).get('general', {}).get('log_max_kb', 1024)
|
||||
log_max_kb = cfg.get('radius', {}).get('general', {}).get('log_max_kb', 1024)
|
||||
size_kb = os.path.getsize(RADIUS_LOG_FILE) / 1024
|
||||
with open(RADIUS_LOG_FILE) as f:
|
||||
lines = f.readlines()
|
||||
|
|
@ -848,7 +909,7 @@ def collect_tokens():
|
|||
tokens['RADIUS_SECRET'] = open(f'{CONFIGS_DIR}/.radius-secret').read().strip()
|
||||
except OSError:
|
||||
tokens['RADIUS_SECRET'] = '(Generation is pending - visit Actions to apply generation command)'
|
||||
_fr = cfg.get('free_radius', {})
|
||||
_fr = cfg.get('radius', {})
|
||||
_fr_opts = _fr.get('options', {})
|
||||
_fr_gen = _fr.get('general', {})
|
||||
tokens['RADIUS_MAC_FORMAT'] = _fr_opts.get('mac_format', 'aabbccddeeff')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue