Development
This commit is contained in:
parent
705c69abc4
commit
470cc39356
5 changed files with 244 additions and 245 deletions
|
|
@ -25,16 +25,6 @@ def _hash_ok():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _flat_index_to_vlan_res(vlans, flat_idx):
|
|
||||||
pos = 0
|
|
||||||
for vi, vlan in enumerate(vlans):
|
|
||||||
for ri in range(len(vlan.get('reservations', []))):
|
|
||||||
if pos == flat_idx:
|
|
||||||
return vi, ri
|
|
||||||
pos += 1
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_ip():
|
def _parse_ip():
|
||||||
raw = request.form.get('ip', '').strip()
|
raw = request.form.get('ip', '').strip()
|
||||||
if not raw:
|
if not raw:
|
||||||
|
|
@ -67,7 +57,7 @@ def addreservation_add():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
vlans = cfg.get('vlans', [])
|
vlans = cfg.get('vlans', [])
|
||||||
vlan = next((v for v in vlans if v.get('name') == vlan_name), None)
|
vlan = next((v for v in vlans if v.get('name') == vlan_name), None)
|
||||||
if vlan is None:
|
if vlan is None:
|
||||||
|
|
@ -86,8 +76,9 @@ def addreservation_add():
|
||||||
'ip': ip,
|
'ip': ip,
|
||||||
'radius_client': radius_client,
|
'radius_client': radius_client,
|
||||||
'enabled': True,
|
'enabled': True,
|
||||||
|
'vlan': vlan_name,
|
||||||
}
|
}
|
||||||
vlan.setdefault('reservations', []).append(entry)
|
cfg.setdefault('dhcp_reservations', []).append(entry)
|
||||||
errors = validate.validate_config(cfg)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
|
|
@ -95,7 +86,7 @@ def addreservation_add():
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
changes = diff_fields(None, entry)
|
changes = diff_fields(None, entry)
|
||||||
flash(record_group(cfg, f'vlans[name={vlan_name}].reservations', 'mac', mac, changes, 'core apply'), 'success')
|
flash(record_group(cfg, 'dhcp_reservations', 'mac', mac, changes, 'core apply'), 'success')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -109,14 +100,13 @@ def reservations_toggle():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
vlans = cfg.get('vlans', [])
|
items = cfg.get('dhcp_reservations', [])
|
||||||
vi, ri = _flat_index_to_vlan_res(vlans, idx)
|
if idx < 0 or idx >= len(items):
|
||||||
if vi is None:
|
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
res = vlans[vi]['reservations'][ri]
|
res = items[idx]
|
||||||
old_enabled = res.get('enabled', True)
|
old_enabled = res.get('enabled', True)
|
||||||
before = copy.deepcopy(res)
|
before = copy.deepcopy(res)
|
||||||
res['enabled'] = not old_enabled
|
res['enabled'] = not old_enabled
|
||||||
|
|
@ -126,9 +116,8 @@ def reservations_toggle():
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
vlan_name = vlans[vi]['name']
|
|
||||||
changes = diff_fields(before, res)
|
changes = diff_fields(before, res)
|
||||||
flash(record_group(cfg, f'vlans[name={vlan_name}].reservations', 'mac', res['mac'], changes, 'core apply'), 'success')
|
flash(record_group(cfg, 'dhcp_reservations', 'mac', res['mac'], changes, 'core apply'), 'success')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -154,19 +143,21 @@ def reservations_edit():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
vlans = cfg.get('vlans', [])
|
items = cfg.get('dhcp_reservations', [])
|
||||||
vi, ri = _flat_index_to_vlan_res(vlans, idx)
|
if idx < 0 or idx >= len(items):
|
||||||
if vi is None:
|
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
conflict = validate.check_reservation_ip_conflicts(ip, vlans[vi])
|
res = items[idx]
|
||||||
if conflict:
|
vlan_name = res.get('vlan', '')
|
||||||
flash(f'The configuration has not been saved because {conflict}', 'error')
|
vlan = next((v for v in cfg.get('vlans', []) if v.get('name') == vlan_name), None)
|
||||||
return redirect(f'/{_PAGE}')
|
if vlan:
|
||||||
|
conflict = validate.check_reservation_ip_conflicts(ip, vlan)
|
||||||
|
if conflict:
|
||||||
|
flash(f'The configuration has not been saved because {conflict}', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
res = vlans[vi]['reservations'][ri]
|
|
||||||
before = copy.deepcopy(res)
|
before = copy.deepcopy(res)
|
||||||
res.update({
|
res.update({
|
||||||
'description': description,
|
'description': description,
|
||||||
|
|
@ -182,9 +173,8 @@ def reservations_edit():
|
||||||
flash(msg, 'error')
|
flash(msg, 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
vlan_name = vlans[vi]['name']
|
|
||||||
changes = diff_fields(before, res)
|
changes = diff_fields(before, res)
|
||||||
flash(record_group(cfg, f'vlans[name={vlan_name}].reservations', 'mac', mac, changes, 'core apply'), 'success')
|
flash(record_group(cfg, 'dhcp_reservations', 'mac', mac, changes, 'core apply'), 'success')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -198,15 +188,13 @@ def reservations_delete():
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
vlans = cfg.get('vlans', [])
|
items = cfg.get('dhcp_reservations', [])
|
||||||
vi, ri = _flat_index_to_vlan_res(vlans, idx)
|
if idx < 0 or idx >= len(items):
|
||||||
if vi is None:
|
|
||||||
flash('Entry not found.', 'error')
|
flash('Entry not found.', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
vlan_name = vlans[vi]['name']
|
removed = items.pop(idx)
|
||||||
removed = vlans[vi]['reservations'].pop(ri)
|
|
||||||
errors = validate.validate_config(cfg)
|
errors = validate.validate_config(cfg)
|
||||||
if errors:
|
if errors:
|
||||||
for msg in errors:
|
for msg in errors:
|
||||||
|
|
@ -214,5 +202,5 @@ def reservations_delete():
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
changes = diff_fields(removed, None)
|
changes = diff_fields(removed, None)
|
||||||
flash(record_group(cfg, f'vlans[name={vlan_name}].reservations', 'mac', removed['mac'], changes, 'core apply'), 'success')
|
flash(record_group(cfg, 'dhcp_reservations', 'mac', removed['mac'], changes, 'core apply'), 'success')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
|
||||||
|
|
@ -334,11 +334,10 @@ def config_datasource(name):
|
||||||
|
|
||||||
if name == 'dhcp_reservations':
|
if name == 'dhcp_reservations':
|
||||||
rows = []
|
rows = []
|
||||||
for vlan in vlans:
|
for res in cfg.get('dhcp_reservations', []):
|
||||||
for res in vlan.get('reservations', []):
|
row = dict(res)
|
||||||
row = dict(res)
|
row['vlan_name'] = res.get('vlan', '-')
|
||||||
row['vlan_name'] = vlan.get('name', '-')
|
rows.append(row)
|
||||||
rows.append(row)
|
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
if name == 'ddns_providers':
|
if name == 'ddns_providers':
|
||||||
|
|
@ -795,10 +794,9 @@ def collect_tokens():
|
||||||
_vn = _v.get('name', '')
|
_vn = _v.get('name', '')
|
||||||
if not _vn:
|
if not _vn:
|
||||||
continue
|
continue
|
||||||
_res_ips_by_vlan[_vn] = [r['ip'] for r in _v.get('reservations', [])
|
_vlan_res = [r for r in cfg.get('dhcp_reservations', []) if r.get('vlan') == _vn]
|
||||||
if r.get('ip') and r['ip'] != 'dynamic']
|
_res_ips_by_vlan[_vn] = [r['ip'] for r in _vlan_res if r.get('ip') and r['ip'] != 'dynamic']
|
||||||
_res_hosts_by_vlan[_vn] = [r['hostname'] for r in _v.get('reservations', [])
|
_res_hosts_by_vlan[_vn] = [r['hostname'] for r in _vlan_res if r.get('hostname')]
|
||||||
if r.get('hostname')]
|
|
||||||
tokens['RESERVATION_IPS_BY_VLAN_JSON'] = json.dumps(_res_ips_by_vlan)
|
tokens['RESERVATION_IPS_BY_VLAN_JSON'] = json.dumps(_res_ips_by_vlan)
|
||||||
tokens['RESERVATION_HOSTNAMES_BY_VLAN_JSON'] = json.dumps(_res_hosts_by_vlan)
|
tokens['RESERVATION_HOSTNAMES_BY_VLAN_JSON'] = json.dumps(_res_hosts_by_vlan)
|
||||||
tokens['EXISTING_VLAN_IDS_JSON'] = json.dumps([v.get('vlan_id') for v in vlans])
|
tokens['EXISTING_VLAN_IDS_JSON'] = json.dumps([v.get('vlan_id') for v in vlans])
|
||||||
|
|
|
||||||
|
|
@ -317,54 +317,6 @@
|
||||||
"ntp_servers": ""
|
"ntp_servers": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reservations": [
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "UniFi Switch",
|
|
||||||
"hostname": "unifi-switch",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:01",
|
|
||||||
"ip": "192.168.1.2",
|
|
||||||
"radius_client": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "UniFi AP (Kitchen)",
|
|
||||||
"hostname": "unifi-ap-kitchen",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:02",
|
|
||||||
"ip": "192.168.1.3",
|
|
||||||
"radius_client": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "UniFi AP (Lounge)",
|
|
||||||
"hostname": "unifi-ap-lounge",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:03",
|
|
||||||
"ip": "192.168.1.4",
|
|
||||||
"radius_client": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "UniFi AP (Upstairs)",
|
|
||||||
"hostname": "unifi-ap-upstairs",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:04",
|
|
||||||
"ip": "192.168.1.5",
|
|
||||||
"radius_client": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Home Server",
|
|
||||||
"hostname": "homeserver",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:05",
|
|
||||||
"ip": "192.168.1.20"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Desktop PC",
|
|
||||||
"hostname": "desktop-pc",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:06",
|
|
||||||
"ip": "192.168.1.50"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"port_wrangling": [
|
"port_wrangling": [
|
||||||
{
|
{
|
||||||
"description": "DNS wrangling - redirect Trusted DNS to local resolver",
|
"description": "DNS wrangling - redirect Trusted DNS to local resolver",
|
||||||
|
|
@ -412,64 +364,6 @@
|
||||||
"ntp_servers": ""
|
"ntp_servers": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reservations": [
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Network Printer",
|
|
||||||
"hostname": "printer",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:10",
|
|
||||||
"ip": "192.168.10.2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Smart TV",
|
|
||||||
"hostname": "smart-tv",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:11",
|
|
||||||
"ip": "192.168.10.3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Streaming Box (Eth)",
|
|
||||||
"hostname": "streaming-box-eth",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:12",
|
|
||||||
"ip": "192.168.10.4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Streaming Box (Wifi)",
|
|
||||||
"hostname": "streaming-box-wifi",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:13",
|
|
||||||
"ip": "192.168.10.4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Raspberry Pi",
|
|
||||||
"hostname": "rpi",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:14",
|
|
||||||
"ip": "192.168.10.12"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "NAS",
|
|
||||||
"hostname": "nas",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:15",
|
|
||||||
"ip": "192.168.10.14"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Doorbell Camera",
|
|
||||||
"hostname": "doorbell-camera",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:16",
|
|
||||||
"ip": "dynamic"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Smart Speaker",
|
|
||||||
"hostname": "smart-speaker",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:17",
|
|
||||||
"ip": "dynamic"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"port_wrangling": [
|
"port_wrangling": [
|
||||||
{
|
{
|
||||||
"description": "DNS wrangling - redirect IoT DNS to local resolver",
|
"description": "DNS wrangling - redirect IoT DNS to local resolver",
|
||||||
|
|
@ -517,22 +411,6 @@
|
||||||
"ntp_servers": ""
|
"ntp_servers": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reservations": [
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Family Member Phone 1",
|
|
||||||
"hostname": "phone-1",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:20",
|
|
||||||
"ip": "dynamic"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Family Member Phone 2",
|
|
||||||
"hostname": "phone-2",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:21",
|
|
||||||
"ip": "dynamic"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"port_wrangling": [
|
"port_wrangling": [
|
||||||
{
|
{
|
||||||
"description": "DNS wrangling - redirect Guest DNS to local resolver",
|
"description": "DNS wrangling - redirect Guest DNS to local resolver",
|
||||||
|
|
@ -581,36 +459,6 @@
|
||||||
"ntp_servers": ""
|
"ntp_servers": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reservations": [
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Child 1 Laptop",
|
|
||||||
"hostname": "child1-laptop",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:30",
|
|
||||||
"ip": "dynamic"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Child 2 Laptop",
|
|
||||||
"hostname": "child2-laptop",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:31",
|
|
||||||
"ip": "dynamic"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Child 3 Laptop",
|
|
||||||
"hostname": "child3-laptop",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:32",
|
|
||||||
"ip": "dynamic"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"description": "Child Tablet",
|
|
||||||
"hostname": "child-tablet",
|
|
||||||
"mac": "aa:bb:cc:dd:ee:33",
|
|
||||||
"ip": "dynamic"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"port_wrangling": [
|
"port_wrangling": [
|
||||||
{
|
{
|
||||||
"description": "DNS wrangling - redirect Kids DNS to local resolver",
|
"description": "DNS wrangling - redirect Kids DNS to local resolver",
|
||||||
|
|
@ -812,5 +660,171 @@
|
||||||
"format": "dnsmasq"
|
"format": "dnsmasq"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
}
|
"dhcp_reservations": [
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "UniFi Switch",
|
||||||
|
"hostname": "unifi-switch",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:01",
|
||||||
|
"ip": "192.168.1.2",
|
||||||
|
"radius_client": true,
|
||||||
|
"vlan": "trusted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "UniFi AP (Kitchen)",
|
||||||
|
"hostname": "unifi-ap-kitchen",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:02",
|
||||||
|
"ip": "192.168.1.3",
|
||||||
|
"radius_client": true,
|
||||||
|
"vlan": "trusted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "UniFi AP (Lounge)",
|
||||||
|
"hostname": "unifi-ap-lounge",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:03",
|
||||||
|
"ip": "192.168.1.4",
|
||||||
|
"radius_client": true,
|
||||||
|
"vlan": "trusted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "UniFi AP (Upstairs)",
|
||||||
|
"hostname": "unifi-ap-upstairs",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:04",
|
||||||
|
"ip": "192.168.1.5",
|
||||||
|
"radius_client": true,
|
||||||
|
"vlan": "trusted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Home Server",
|
||||||
|
"hostname": "homeserver",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:05",
|
||||||
|
"ip": "192.168.1.20",
|
||||||
|
"vlan": "trusted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Desktop PC",
|
||||||
|
"hostname": "desktop-pc",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:06",
|
||||||
|
"ip": "192.168.1.50",
|
||||||
|
"vlan": "trusted"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Network Printer",
|
||||||
|
"hostname": "printer",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:10",
|
||||||
|
"ip": "192.168.10.2",
|
||||||
|
"vlan": "iot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Smart TV",
|
||||||
|
"hostname": "smart-tv",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:11",
|
||||||
|
"ip": "192.168.10.3",
|
||||||
|
"vlan": "iot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Streaming Box (Eth)",
|
||||||
|
"hostname": "streaming-box-eth",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:12",
|
||||||
|
"ip": "192.168.10.4",
|
||||||
|
"vlan": "iot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Streaming Box (Wifi)",
|
||||||
|
"hostname": "streaming-box-wifi",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:13",
|
||||||
|
"ip": "192.168.10.4",
|
||||||
|
"vlan": "iot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Raspberry Pi",
|
||||||
|
"hostname": "rpi",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:14",
|
||||||
|
"ip": "192.168.10.12",
|
||||||
|
"vlan": "iot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "NAS",
|
||||||
|
"hostname": "nas",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:15",
|
||||||
|
"ip": "192.168.10.14",
|
||||||
|
"vlan": "iot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Doorbell Camera",
|
||||||
|
"hostname": "doorbell-camera",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:16",
|
||||||
|
"ip": "dynamic",
|
||||||
|
"vlan": "iot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Smart Speaker",
|
||||||
|
"hostname": "smart-speaker",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:17",
|
||||||
|
"ip": "dynamic",
|
||||||
|
"vlan": "iot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Family Member Phone 1",
|
||||||
|
"hostname": "phone-1",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:20",
|
||||||
|
"ip": "dynamic",
|
||||||
|
"vlan": "guest"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Family Member Phone 2",
|
||||||
|
"hostname": "phone-2",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:21",
|
||||||
|
"ip": "dynamic",
|
||||||
|
"vlan": "guest"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Child 1 Laptop",
|
||||||
|
"hostname": "child1-laptop",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:30",
|
||||||
|
"ip": "dynamic",
|
||||||
|
"vlan": "kids"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Child 2 Laptop",
|
||||||
|
"hostname": "child2-laptop",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:31",
|
||||||
|
"ip": "dynamic",
|
||||||
|
"vlan": "kids"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Child 3 Laptop",
|
||||||
|
"hostname": "child3-laptop",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:32",
|
||||||
|
"ip": "dynamic",
|
||||||
|
"vlan": "kids"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Child Tablet",
|
||||||
|
"hostname": "child-tablet",
|
||||||
|
"mac": "aa:bb:cc:dd:ee:33",
|
||||||
|
"ip": "dynamic",
|
||||||
|
"vlan": "kids"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -492,8 +492,9 @@ def build_vlan_dnsmasq_conf(vlan, data, iface):
|
||||||
line(f"dhcp-host={s['ip']},{s['hostname']}")
|
line(f"dhcp-host={s['ip']},{s['hostname']}")
|
||||||
line()
|
line()
|
||||||
|
|
||||||
active_res = [r for r in vlan.get("reservations", []) if r.get("enabled") is True]
|
vlan_res = [r for r in data.get("dhcp_reservations", []) if r.get("vlan") == name]
|
||||||
inactive_res = [r for r in vlan.get("reservations", []) if r.get("enabled") is not True]
|
active_res = [r for r in vlan_res if r.get("enabled") is True]
|
||||||
|
inactive_res = [r for r in vlan_res if r.get("enabled") is not True]
|
||||||
|
|
||||||
if active_res:
|
if active_res:
|
||||||
line("# -- Reservations -----------------------------------------------")
|
line("# -- Reservations -----------------------------------------------")
|
||||||
|
|
@ -1821,12 +1822,12 @@ RADIUS_USERS_FILE = Path("/etc/freeradius/3.0/users")
|
||||||
|
|
||||||
def radius_clients(data):
|
def radius_clients(data):
|
||||||
"""Return list of (reservation, vlan) tuples where radius_client is True."""
|
"""Return list of (reservation, vlan) tuples where radius_client is True."""
|
||||||
result = []
|
vlan_by_name = {v["name"]: v for v in data.get("vlans", [])}
|
||||||
for vlan in data["vlans"]:
|
return [
|
||||||
for r in vlan.get("reservations", []):
|
(r, vlan_by_name[r["vlan"]])
|
||||||
if r.get("radius_client") is True:
|
for r in data.get("dhcp_reservations", [])
|
||||||
result.append((r, vlan))
|
if r.get("radius_client") is True and r.get("vlan") in vlan_by_name
|
||||||
return result
|
]
|
||||||
|
|
||||||
def radius_enabled(data):
|
def radius_enabled(data):
|
||||||
"""Return True if any reservation has radius_client: true."""
|
"""Return True if any reservation has radius_client: true."""
|
||||||
|
|
@ -1889,22 +1890,25 @@ def build_radius_users(data):
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
for vlan in data["vlans"]:
|
vlan_by_name = {v["name"]: v for v in data.get("vlans", [])}
|
||||||
|
for r in data.get("dhcp_reservations", []):
|
||||||
|
if r.get("enabled") is not True:
|
||||||
|
continue
|
||||||
|
mac = r.get("mac", "").replace(":", "").lower()
|
||||||
|
if not mac:
|
||||||
|
continue
|
||||||
|
vlan = vlan_by_name.get(r.get("vlan", ""))
|
||||||
|
if not vlan:
|
||||||
|
continue
|
||||||
vlan_id = vlan.get('vlan_id')
|
vlan_id = vlan.get('vlan_id')
|
||||||
for r in vlan.get("reservations", []):
|
lines += [
|
||||||
if r.get("enabled") is not True:
|
f"# {r['description']} -> VLAN {vlan_id} ({vlan['name']})",
|
||||||
continue
|
f"{mac} Cleartext-Password := \"{mac}\"",
|
||||||
mac = r.get("mac", "").replace(":", "").lower()
|
f" Tunnel-Type = VLAN,",
|
||||||
if not mac:
|
f" Tunnel-Medium-Type = IEEE-802,",
|
||||||
continue
|
f" Tunnel-Private-Group-Id = \"{vlan_id}\"",
|
||||||
lines += [
|
"",
|
||||||
f"# {r['description']} -> VLAN {vlan_id} ({vlan['name']})",
|
]
|
||||||
f"{mac} Cleartext-Password := \"{mac}\"",
|
|
||||||
f" Tunnel-Type = VLAN,",
|
|
||||||
f" Tunnel-Medium-Type = IEEE-802,",
|
|
||||||
f" Tunnel-Private-Group-Id = \"{vlan_id}\"",
|
|
||||||
"",
|
|
||||||
]
|
|
||||||
|
|
||||||
default_id = default_vlan.get('vlan_id')
|
default_id = default_vlan.get('vlan_id')
|
||||||
lines += [
|
lines += [
|
||||||
|
|
@ -2143,13 +2147,13 @@ def reset_leases(data, vlan_name=None):
|
||||||
|
|
||||||
def show_leases(data):
|
def show_leases(data):
|
||||||
# Build MAC -> reservation lookup across all VLANs
|
# Build MAC -> reservation lookup across all VLANs
|
||||||
|
vlan_by_name = {v["name"]: v for v in data.get("vlans", [])}
|
||||||
res_by_mac = {}
|
res_by_mac = {}
|
||||||
for vlan in data["vlans"]:
|
for r in data.get("dhcp_reservations", []):
|
||||||
for r in vlan.get("reservations", []):
|
if r.get("enabled") is True:
|
||||||
if r.get("enabled") is True:
|
mac = r.get("mac", "").lower().strip()
|
||||||
mac = r.get("mac", "").lower().strip()
|
if mac:
|
||||||
if mac:
|
res_by_mac[mac] = (r, vlan_by_name.get(r.get("vlan", ""), {}))
|
||||||
res_by_mac[mac] = (r, vlan)
|
|
||||||
|
|
||||||
now = int(datetime.now().timestamp())
|
now = int(datetime.now().timestamp())
|
||||||
any_leases = False
|
any_leases = False
|
||||||
|
|
@ -2922,10 +2926,7 @@ def cmd_apply(data, dry_run=False):
|
||||||
print("RADIUS (dry-run) ====================================================")
|
print("RADIUS (dry-run) ====================================================")
|
||||||
num_clients = len(radius_clients(data))
|
num_clients = len(radius_clients(data))
|
||||||
default_vlan = next((v for v in data["vlans"] if v.get("radius_default") is True), None)
|
default_vlan = next((v for v in data["vlans"] if v.get("radius_default") is True), None)
|
||||||
total_macs = sum(
|
total_macs = len([r for r in data.get("dhcp_reservations", []) if r.get("enabled") is True])
|
||||||
len([r for r in v.get("reservations", []) if r.get("enabled") is True])
|
|
||||||
for v in data["vlans"]
|
|
||||||
)
|
|
||||||
print(f" Would write: {RADIUS_CLIENTS_CONF}")
|
print(f" Would write: {RADIUS_CLIENTS_CONF}")
|
||||||
print(f" {num_clients} RADIUS client(s)")
|
print(f" {num_clients} RADIUS client(s)")
|
||||||
print(f" Would write: {RADIUS_USERS_FILE}")
|
print(f" Would write: {RADIUS_USERS_FILE}")
|
||||||
|
|
@ -2944,14 +2945,10 @@ def cmd_apply(data, dry_run=False):
|
||||||
|
|
||||||
check_root()
|
check_root()
|
||||||
|
|
||||||
total_enabled = sum(
|
wg_names = {v["name"] for v in data["vlans"] if is_wg(v)}
|
||||||
len([r for r in v.get("reservations", []) if r.get("enabled") is True])
|
non_wg_res = [r for r in data.get("dhcp_reservations", []) if r.get("vlan") not in wg_names]
|
||||||
for v in data["vlans"] if not is_wg(v)
|
total_enabled = len([r for r in non_wg_res if r.get("enabled") is True])
|
||||||
)
|
total_disabled = len([r for r in non_wg_res if r.get("enabled") is not True])
|
||||||
total_disabled = sum(
|
|
||||||
len([r for r in v.get("reservations", []) if r.get("enabled") is not True])
|
|
||||||
for v in data["vlans"] if not is_wg(v)
|
|
||||||
)
|
|
||||||
total_wg_peers = sum(len(v.get("peers", [])) for v in data["vlans"] if is_wg(v))
|
total_wg_peers = sum(len(v.get("peers", [])) for v in data["vlans"] if is_wg(v))
|
||||||
wg_part = f", {total_wg_peers} WG peer(s)" if total_wg_peers else ""
|
wg_part = f", {total_wg_peers} WG peer(s)" if total_wg_peers else ""
|
||||||
print(f"Applying config: {len(data['vlans'])} VLAN(s), "
|
print(f"Applying config: {len(data['vlans'])} VLAN(s), "
|
||||||
|
|
|
||||||
|
|
@ -741,7 +741,8 @@ def validate_config(data):
|
||||||
|
|
||||||
seen_res_ips = {}
|
seen_res_ips = {}
|
||||||
seen_res_macs = {}
|
seen_res_macs = {}
|
||||||
for r in vlan.get("reservations", []):
|
vlan_name_key = vlan.get("name", "")
|
||||||
|
for r in [r for r in data.get("dhcp_reservations", []) if r.get("vlan") == vlan_name_key]:
|
||||||
rdesc = r.get("description", "?")
|
rdesc = r.get("description", "?")
|
||||||
rmac = r.get("mac", "").lower().strip()
|
rmac = r.get("mac", "").lower().strip()
|
||||||
|
|
||||||
|
|
@ -876,10 +877,11 @@ def validate_config(data):
|
||||||
|
|
||||||
# RADIUS requires multiple VLANs ================================
|
# RADIUS requires multiple VLANs ================================
|
||||||
non_wg_vlans = [v for v in data.get("vlans", []) if not is_wg(v)]
|
non_wg_vlans = [v for v in data.get("vlans", []) if not is_wg(v)]
|
||||||
|
non_wg_names = {v.get("name") for v in non_wg_vlans}
|
||||||
has_radius_clients = any(
|
has_radius_clients = any(
|
||||||
r.get("radius_client")
|
r.get("radius_client")
|
||||||
for v in non_wg_vlans
|
for r in data.get("dhcp_reservations", [])
|
||||||
for r in v.get("reservations", [])
|
if r.get("vlan") in non_wg_names
|
||||||
)
|
)
|
||||||
if has_radius_clients and len(non_wg_vlans) < 2:
|
if has_radius_clients and len(non_wg_vlans) < 2:
|
||||||
errors.append(
|
errors.append(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue