diff --git a/docker/routlin-dash/app/action_networklayout.py b/docker/routlin-dash/app/action_networklayout.py index d1378f0..4064189 100644 --- a/docker/routlin-dash/app/action_networklayout.py +++ b/docker/routlin-dash/app/action_networklayout.py @@ -1,4 +1,5 @@ import copy +import ipaddress from flask import Blueprint, request, redirect, flash from auth import require_level @@ -209,18 +210,52 @@ def networklayout_tablevlans_edit(): existing_gw = existing.get('dhcp_information', {}).get('explicit_overrides', {}).get('gateway', '') dns_override = 'dns_server_override' in request.form - dns_raw = sanitize.ip(request.form.get('dns_server', '')) - if dns_override: - if not dns_raw: - flash('DNS server IP is required when override is enabled.', 'error') + dns_ips = [] + for _line in request.form.get('dns_server', '').splitlines(): + _line = _line.strip() + if not _line: + continue + _clean = sanitize.ip(_line) + if not _clean: + flash(f"'{_line}' is not a valid DNS server IP.", 'error') return redirect(VIEW) - if dns_raw not in identity_ips: - flash(f"DNS server '{dns_raw}' must match one of the server identity IPs.", 'error') + dns_ips.append(_clean) + if dns_override and not dns_ips: + flash('At least one DNS server IP is required when override is enabled.', 'error') + return redirect(VIEW) + if dns_override and dns_ips: + _vlan_net = ipaddress.IPv4Network(f'{subnet}/{final_mask}', strict=False) + for _ip in dns_ips: + if ipaddress.IPv4Address(_ip) not in _vlan_net: + flash(f"DNS server '{_ip}' is not in the VLAN subnet ({subnet}/{final_mask}).", 'error') + return redirect(VIEW) + new_stored_dns = dns_ips if dns_override else [] + _existing_dns = existing.get('dhcp_information', {}).get('explicit_overrides', {}).get('dns_server', []) + existing_dns = _existing_dns if isinstance(_existing_dns, list) else ([_existing_dns] if _existing_dns else []) + + ntp_override = 'ntp_server_override' in request.form + ntp_ips = [] + for _line in request.form.get('ntp_server', '').splitlines(): + _line = _line.strip() + if not _line: + continue + _clean = sanitize.ip(_line) + if not _clean: + flash(f"'{_line}' is not a valid NTP server IP.", 'error') return redirect(VIEW) - inferred_dns = (min(identity_ips, key=lambda ip: int(ip.split('.')[-1])) - if identity_ips else '') - new_stored_dns = dns_raw if (dns_override and dns_raw and dns_raw != inferred_dns) else '' - existing_dns = existing.get('dhcp_information', {}).get('explicit_overrides', {}).get('dns_server', '') + ntp_ips.append(_clean) + if ntp_override and not ntp_ips: + flash('At least one NTP server IP is required when override is enabled.', 'error') + return redirect(VIEW) + if ntp_override and ntp_ips: + _vlan_net = ipaddress.IPv4Network(f'{subnet}/{final_mask}', strict=False) + for _ip in ntp_ips: + if ipaddress.IPv4Address(_ip) not in _vlan_net: + flash(f"NTP server '{_ip}' is not in the VLAN subnet ({subnet}/{final_mask}).", 'error') + return redirect(VIEW) + new_stored_ntp = ntp_ips if ntp_override else [] + _existing_ntp = existing.get('dhcp_information', {}).get('explicit_overrides', {}).get('ntp_server', []) + existing_ntp = _existing_ntp if isinstance(_existing_ntp, list) else ([_existing_ntp] if _existing_ntp else []) _ids_unchanged = ( len(new_identities) == len(old_identities) and @@ -240,7 +275,8 @@ def networklayout_tablevlans_edit(): and sorted(use_blocklists) == sorted(existing.get('use_blocklists', [])) and _ids_unchanged and new_stored_gw == existing_gw - and new_stored_dns == existing_dns): + and new_stored_dns == existing_dns + and new_stored_ntp == existing_ntp): flash('No changes were made.', 'info') return redirect(VIEW) @@ -265,6 +301,12 @@ def networklayout_tablevlans_edit(): dhcp_overrides['dns_server'] = new_stored_dns else: dhcp_overrides.pop('dns_server', None) + if new_stored_ntp: + dhcp_overrides['ntp_server'] = new_stored_ntp + else: + dhcp_overrides.pop('ntp_server', None) + if not dhcp_overrides: + existing.get('dhcp_information', {}).pop('explicit_overrides', None) errors = validate.validate_config(cfg) if errors: for msg in errors: diff --git a/docker/routlin-dash/app/view_page.py b/docker/routlin-dash/app/view_page.py index d1e510a..5617fc8 100644 --- a/docker/routlin-dash/app/view_page.py +++ b/docker/routlin-dash/app/view_page.py @@ -325,9 +325,10 @@ def _config_datasource(name): row['server_identity_gateway'] = ( v.get('dhcp_information', {}).get('explicit_overrides', {}).get('gateway', '') ) - row['server_identity_dns_server'] = ( - v.get('dhcp_information', {}).get('explicit_overrides', {}).get('dns_server', '') - ) + _dns = v.get('dhcp_information', {}).get('explicit_overrides', {}).get('dns_server', []) + row['server_identity_dns_server'] = '\n'.join(_dns) if isinstance(_dns, list) else str(_dns or '') + _ntp = v.get('dhcp_information', {}).get('explicit_overrides', {}).get('ntp_server', []) + row['server_identity_ntp_server'] = '\n'.join(_ntp) if isinstance(_ntp, list) else str(_ntp or '') rows.append(row) return rows diff --git a/docker/routlin-dash/data/page_content.json b/docker/routlin-dash/data/page_content.json index 22de853..ab87f20 100644 --- a/docker/routlin-dash/data/page_content.json +++ b/docker/routlin-dash/data/page_content.json @@ -1589,7 +1589,8 @@ "pair_label2": "Hostname (Opt)", "pair_validate2": "networkname", "gateway_col": "server_identity_gateway", - "dns_col": "server_identity_dns_server" + "dns_col": "server_identity_dns_server", + "ntp_col": "server_identity_ntp_server" }, { "col": "radius_default",