Development
This commit is contained in:
parent
8f377b1839
commit
33b639a353
4 changed files with 85 additions and 8 deletions
|
|
@ -1,8 +1,10 @@
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from flask import Blueprint, request, redirect, flash, session
|
from flask import Blueprint, request, redirect, flash, session
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import (flush_pending_to_queue, get_dashboard_pending,
|
from config_utils import (flush_pending_to_queue, get_dashboard_pending,
|
||||||
revert_snapshot_to_config, queued_msg)
|
revert_snapshot_to_config, queued_msg,
|
||||||
|
SNAPSHOTS_DIR, DASHBOARD_PENDING)
|
||||||
|
|
||||||
_PAGE = Path(__file__).parent.name
|
_PAGE = Path(__file__).parent.name
|
||||||
|
|
||||||
|
|
@ -48,3 +50,28 @@ def history_revert():
|
||||||
plural = 's' if succeeded != 1 else ''
|
plural = 's' if succeeded != 1 else ''
|
||||||
flash(f'{succeeded} change{plural} reverted.', 'success')
|
flash(f'{succeeded} change{plural} reverted.', 'success')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/action/actions/history_clear', methods=['POST'])
|
||||||
|
@require_level('manager')
|
||||||
|
def history_clear():
|
||||||
|
count = 0
|
||||||
|
for fname in os.listdir(SNAPSHOTS_DIR):
|
||||||
|
fpath = os.path.join(SNAPSHOTS_DIR, fname)
|
||||||
|
if os.path.isfile(fpath):
|
||||||
|
os.remove(fpath)
|
||||||
|
count += 1
|
||||||
|
plural = 's' if count != 1 else ''
|
||||||
|
flash(f'History cleared ({count} record{plural} removed).', 'success')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/action/actions/pending_dismiss', methods=['POST'])
|
||||||
|
@require_level('manager')
|
||||||
|
def pending_dismiss():
|
||||||
|
if not get_dashboard_pending():
|
||||||
|
flash('No pending changes to dismiss.', 'info')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
open(DASHBOARD_PENDING, 'w').close()
|
||||||
|
flash('Pending changes dismissed.', 'success')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,18 @@
|
||||||
{
|
{
|
||||||
"type": "raw_html",
|
"type": "raw_html",
|
||||||
"html": "%APPLY_WARNING%"
|
"html": "%APPLY_WARNING%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "raw_html",
|
||||||
|
"html": "<span style='flex:1'></span>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_danger",
|
||||||
|
"text": "Dismiss All",
|
||||||
|
"action": "/action/actions/pending_dismiss",
|
||||||
|
"method": "post",
|
||||||
|
"disabled": "%NO_PENDING%",
|
||||||
|
"client_requirement": "client_is_manager="
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -95,11 +107,20 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "button_row",
|
"type": "button_row",
|
||||||
|
"justify": "space-between",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "button_danger",
|
"type": "button_secondary",
|
||||||
"text": "Revert Selected",
|
"text": "Revert Selected",
|
||||||
"disabled": "%NO_HISTORY%"
|
"disabled": "%NO_HISTORY%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_danger",
|
||||||
|
"text": "Clear History",
|
||||||
|
"action": "/action/actions/history_clear",
|
||||||
|
"method": "post",
|
||||||
|
"disabled": "%NO_HISTORY%",
|
||||||
|
"client_requirement": "client_is_manager="
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,14 @@ def addvlan_add():
|
||||||
if subnet_mask is None:
|
if subnet_mask is None:
|
||||||
flash('Invalid subnet prefix (must be 1-30).', 'error')
|
flash('Invalid subnet prefix (must be 1-30).', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
if is_vpn and mdns_reflection:
|
||||||
|
flash('mDNS reflection is not supported on VPN VLANs.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
_vlan_net = ipaddress.IPv4Network(f'{subnet}/{subnet_mask}', strict=False)
|
||||||
|
if ipaddress.IPv4Address(subnet) != _vlan_net.network_address:
|
||||||
|
flash(f"Subnet IP must be a network address (expected {_vlan_net.network_address}).", 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
if not _hash_ok():
|
if not _hash_ok():
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
@ -73,16 +81,18 @@ def addvlan_add():
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
new_identities = []
|
new_identities = []
|
||||||
if raw_identities:
|
for raw in raw_identities:
|
||||||
_vlan_net = ipaddress.IPv4Network(f'{subnet}/{subnet_mask}', strict=False)
|
|
||||||
for raw in raw_identities:
|
|
||||||
ip_clean = sanitize.ip(str(raw.get('ip', '')))
|
ip_clean = sanitize.ip(str(raw.get('ip', '')))
|
||||||
if not ip_clean:
|
if not ip_clean:
|
||||||
flash('Invalid IP address in identity.', 'error')
|
flash('Invalid IP address in identity.', 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
if ipaddress.IPv4Address(ip_clean) not in _vlan_net:
|
_addr = ipaddress.IPv4Address(ip_clean)
|
||||||
|
if _addr not in _vlan_net:
|
||||||
flash(f"Identity IP '{ip_clean}' is not in the VLAN subnet ({subnet}/{subnet_mask}).", 'error')
|
flash(f"Identity IP '{ip_clean}' is not in the VLAN subnet ({subnet}/{subnet_mask}).", 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
if _addr == _vlan_net.network_address or _addr == _vlan_net.broadcast_address:
|
||||||
|
flash(f"Identity IP '{ip_clean}' cannot be the network or broadcast address.", 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
ident = {'ip': ip_clean}
|
ident = {'ip': ip_clean}
|
||||||
desc = str(raw.get('description', '')).strip()
|
desc = str(raw.get('description', '')).strip()
|
||||||
if desc:
|
if desc:
|
||||||
|
|
@ -116,6 +126,11 @@ def addvlan_add():
|
||||||
flash(f"'{_line}' is not a valid DNS server IP.", 'error')
|
flash(f"'{_line}' is not a valid DNS server IP.", 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
dns_ips.append(_clean)
|
dns_ips.append(_clean)
|
||||||
|
if dns_override and dns_ips:
|
||||||
|
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}/{subnet_mask}).", 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
new_stored_dns = dns_ips if dns_override else []
|
new_stored_dns = dns_ips if dns_override else []
|
||||||
|
|
||||||
ntp_override = 'ntp_server_override' in request.form
|
ntp_override = 'ntp_server_override' in request.form
|
||||||
|
|
@ -129,6 +144,11 @@ def addvlan_add():
|
||||||
flash(f"'{_line}' is not a valid NTP server IP.", 'error')
|
flash(f"'{_line}' is not a valid NTP server IP.", 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
ntp_ips.append(_clean)
|
ntp_ips.append(_clean)
|
||||||
|
if ntp_override and ntp_ips:
|
||||||
|
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}/{subnet_mask}).", 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
new_stored_ntp = ntp_ips if ntp_override else []
|
new_stored_ntp = ntp_ips if ntp_override else []
|
||||||
|
|
||||||
dhcp_domain_raw = request.form.get('dhcp_domain', '').strip()
|
dhcp_domain_raw = request.form.get('dhcp_domain', '').strip()
|
||||||
|
|
@ -254,12 +274,20 @@ def vlans_edit():
|
||||||
is_vpn = existing.get('is_vpn', False)
|
is_vpn = existing.get('is_vpn', False)
|
||||||
final_mask = subnet_mask if subnet_mask is not None else existing.get('subnet_mask', 24)
|
final_mask = subnet_mask if subnet_mask is not None else existing.get('subnet_mask', 24)
|
||||||
|
|
||||||
|
if is_vpn and mdns_reflection:
|
||||||
|
flash('mDNS reflection is not supported on VPN VLANs.', 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
if identity_ips:
|
if identity_ips:
|
||||||
_vlan_net = ipaddress.IPv4Network(f'{subnet}/{final_mask}', strict=False)
|
_vlan_net = ipaddress.IPv4Network(f'{subnet}/{final_mask}', strict=False)
|
||||||
for _ip in identity_ips:
|
for _ip in identity_ips:
|
||||||
if ipaddress.IPv4Address(_ip) not in _vlan_net:
|
_addr = ipaddress.IPv4Address(_ip)
|
||||||
|
if _addr not in _vlan_net:
|
||||||
flash(f"Server identity IP '{_ip}' is not in the VLAN subnet ({subnet}/{final_mask}).", 'error')
|
flash(f"Server identity IP '{_ip}' is not in the VLAN subnet ({subnet}/{final_mask}).", 'error')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
if _addr == _vlan_net.network_address or _addr == _vlan_net.broadcast_address:
|
||||||
|
flash(f"Server identity IP '{_ip}' cannot be the network or broadcast address.", 'error')
|
||||||
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
current_id = existing.get('vlan_id')
|
current_id = existing.get('vlan_id')
|
||||||
if current_id == 1 and vlan_id != 1:
|
if current_id == 1 and vlan_id != 1:
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,8 @@
|
||||||
"name": "mac",
|
"name": "mac",
|
||||||
"input_type": "text",
|
"input_type": "text",
|
||||||
"validate": "mac",
|
"validate": "mac",
|
||||||
"value": ""
|
"value": "",
|
||||||
|
"hint": "Factory default: none"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue