Development
This commit is contained in:
parent
c5b02b1289
commit
6f23a57220
7 changed files with 505 additions and 3 deletions
|
|
@ -5,7 +5,7 @@ from view_page import bp as view_page_bp
|
|||
from pages.actions.action import bp as actions_bp
|
||||
from pages.bannedips.action import bp as bannedips_bp
|
||||
from pages.ddns.action import bp as ddns_bp
|
||||
from pages.dhcp.action import bp as dhcp_bp
|
||||
from pages.dhcpreservations.action import bp as dhcpreservations_bp
|
||||
from pages.dnsblocking.action import bp as dnsblocking_bp
|
||||
from pages.dnsserver.action import bp as dnsserver_bp
|
||||
from pages.hostoverrides.action import bp as hostoverrides_bp
|
||||
|
|
@ -31,7 +31,7 @@ app.register_blueprint(view_page_bp)
|
|||
app.register_blueprint(actions_bp)
|
||||
app.register_blueprint(bannedips_bp)
|
||||
app.register_blueprint(ddns_bp)
|
||||
app.register_blueprint(dhcp_bp)
|
||||
app.register_blueprint(dhcpreservations_bp)
|
||||
app.register_blueprint(dnsblocking_bp)
|
||||
app.register_blueprint(dnsserver_bp)
|
||||
app.register_blueprint(hostoverrides_bp)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
{ "type": "nav_item", "label": "Inter-VLAN Exceptions", "map_to": "intervlan", "client_requirement": "client_is_administrator+" },
|
||||
{ "type": "nav_item", "label": "Port Forwarding", "map_to": "portforwarding", "client_requirement": "client_is_administrator+" },
|
||||
{ "type": "nav_item", "label": "Port Wrangling", "map_to": "portwrangling", "client_requirement": "client_is_administrator+" },
|
||||
{ "type": "nav_item", "label": "DHCP", "map_to": "dhcp" },
|
||||
{ "type": "nav_item", "label": "DHCP Leases", "map_to": "dhcpleases" },
|
||||
{ "type": "nav_item", "label": "DHCP Reservations", "map_to": "dhcpreservations" },
|
||||
{ "type": "nav_item", "label": "DDNS", "map_to": "ddns" },
|
||||
{ "type": "nav_item", "label": "Host Overrides", "map_to": "hostoverrides", "client_requirement": "client_is_administrator+" },
|
||||
{ "type": "nav_item", "label": "VPN", "map_to": "vpn" },
|
||||
|
|
|
|||
0
docker/routlin-dash/app/pages/dhcpleases/__init__.py
Normal file
0
docker/routlin-dash/app/pages/dhcpleases/__init__.py
Normal file
58
docker/routlin-dash/app/pages/dhcpleases/content.json
Normal file
58
docker/routlin-dash/app/pages/dhcpleases/content.json
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{
|
||||
"type": "header_page_title",
|
||||
"items": [
|
||||
{
|
||||
"type": "h1",
|
||||
"text": "DHCP Leases"
|
||||
},
|
||||
{
|
||||
"type": "p",
|
||||
"text": "Active leases handed out by dnsmasq across all VLANs."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "table",
|
||||
"datasource": "live:dhcp_leases",
|
||||
"empty_message": "No active DHCP leases found.",
|
||||
"toolbar": {
|
||||
"items": [
|
||||
{
|
||||
"type": "select",
|
||||
"name": "vlan_filter",
|
||||
"value": "all",
|
||||
"filter_col": "vlan_name",
|
||||
"options": "%VLAN_FILTER_OPTIONS%"
|
||||
}
|
||||
]
|
||||
},
|
||||
"columns": [
|
||||
{
|
||||
"label": "Hostname",
|
||||
"field": "hostname"
|
||||
},
|
||||
{
|
||||
"label": "IP Address",
|
||||
"field": "ip_address",
|
||||
"class": "col-mono"
|
||||
},
|
||||
{
|
||||
"label": "MAC Address",
|
||||
"field": "mac_address",
|
||||
"class": "col-mono"
|
||||
},
|
||||
{
|
||||
"label": "VLAN",
|
||||
"field": "vlan_name"
|
||||
},
|
||||
{
|
||||
"label": "Expires",
|
||||
"field": "expires"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
237
docker/routlin-dash/app/pages/dhcpreservations/action.py
Normal file
237
docker/routlin-dash/app/pages/dhcpreservations/action.py
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
from pathlib import Path
|
||||
import copy
|
||||
import ipaddress
|
||||
|
||||
from flask import Blueprint, request, redirect, flash
|
||||
from auth import require_level
|
||||
from config_utils import load_config, record_group, diff_fields, verify_config_hash
|
||||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
def _row_index():
|
||||
try:
|
||||
return int(request.form.get('row_index', ''))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def _hash_ok():
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _parse_ip():
|
||||
raw = request.form.get('ip', '').strip()
|
||||
if not raw:
|
||||
return 'dynamic'
|
||||
ip = validate.ip(raw)
|
||||
if not ip:
|
||||
flash(f'The configuration has not been saved because "{raw}" is not a valid IP address.', 'error')
|
||||
return None
|
||||
return ip
|
||||
|
||||
|
||||
def _check_ip_in_vlan_subnet(ip, vlan):
|
||||
if not ip or ip == 'dynamic':
|
||||
return None
|
||||
subnet = vlan.get('subnet')
|
||||
prefix = vlan.get('subnet_mask')
|
||||
if not subnet or prefix is None:
|
||||
return None
|
||||
try:
|
||||
network = ipaddress.IPv4Network(f'{subnet}/{prefix}', strict=False)
|
||||
addr = ipaddress.IPv4Address(ip)
|
||||
if addr == network.network_address:
|
||||
return f'{ip} is the network address and cannot be assigned.'
|
||||
if addr == network.broadcast_address:
|
||||
return f'{ip} is the broadcast address and cannot be assigned.'
|
||||
if addr not in network:
|
||||
return f'{ip} is not within the {vlan["name"]} subnet ({subnet}/{prefix}).'
|
||||
except ValueError:
|
||||
return f'{ip} is not a valid IP address.'
|
||||
return None
|
||||
|
||||
|
||||
@bp.route('/action/dhcpreservations/addreservation_add', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def addreservation_add():
|
||||
vlan_name = sanitize.name(request.form.get('vlan_name', ''))
|
||||
description = sanitize.text(request.form.get('description', ''))
|
||||
hostname = validate.domainname(request.form.get('hostname', ''))
|
||||
mac = sanitize.mac(request.form.get('mac', ''))
|
||||
ip = _parse_ip()
|
||||
radius_client = 'radius_client' in request.form
|
||||
|
||||
if ip is None:
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not vlan_name:
|
||||
flash('The configuration has not been saved because a VLAN is required.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not mac:
|
||||
flash('The configuration has not been saved because a MAC address is required.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
vlans = cfg.get('vlans', [])
|
||||
vlan = next((v for v in vlans if v.get('name') == vlan_name), None)
|
||||
if vlan is None:
|
||||
flash(f'The configuration has not been saved because VLAN "{vlan_name}" was not found.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
subnet_err = _check_ip_in_vlan_subnet(ip, vlan)
|
||||
if subnet_err:
|
||||
flash(f'The configuration has not been saved because {subnet_err}', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
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}')
|
||||
|
||||
entry = {
|
||||
'description': description,
|
||||
'hostname': hostname,
|
||||
'mac': mac,
|
||||
'ip': ip,
|
||||
'radius_client': radius_client,
|
||||
'enabled': True,
|
||||
'vlan': vlan_name,
|
||||
}
|
||||
cfg.setdefault('dhcp_reservations', []).append(entry)
|
||||
errors = validate.validate_config(cfg)
|
||||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
changes = diff_fields(None, entry)
|
||||
flash(record_group(cfg, 'dhcp_reservations', 'mac', mac, changes, 'core apply'), 'success')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/dhcpreservations/reservations_toggle', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def reservations_toggle():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('dhcp_reservations', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
res = items[idx]
|
||||
old_enabled = res.get('enabled', True)
|
||||
before = copy.deepcopy(res)
|
||||
res['enabled'] = not old_enabled
|
||||
errors = validate.validate_config(cfg)
|
||||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
changes = diff_fields(before, res)
|
||||
flash(record_group(cfg, 'dhcp_reservations', 'mac', res['mac'], changes, 'core apply'), 'success')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/dhcpreservations/reservations_edit', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def reservations_edit():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
description = sanitize.text(request.form.get('description', ''))
|
||||
hostname = validate.domainname(request.form.get('hostname', ''))
|
||||
mac = sanitize.mac(request.form.get('mac', ''))
|
||||
ip = _parse_ip()
|
||||
radius_client = 'radius_client' in request.form
|
||||
|
||||
if ip is None:
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not mac:
|
||||
flash('The configuration has not been saved because a MAC address is required.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('dhcp_reservations', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
res = items[idx]
|
||||
vlan_name = res.get('vlan', '')
|
||||
vlan = next((v for v in cfg.get('vlans', []) if v.get('name') == vlan_name), None)
|
||||
if vlan:
|
||||
subnet_err = _check_ip_in_vlan_subnet(ip, vlan)
|
||||
if subnet_err:
|
||||
flash(f'The configuration has not been saved because {subnet_err}', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
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}')
|
||||
|
||||
before = copy.deepcopy(res)
|
||||
res.update({
|
||||
'description': description,
|
||||
'hostname': hostname,
|
||||
'mac': mac,
|
||||
'ip': ip,
|
||||
'radius_client': radius_client,
|
||||
'enabled': 'enabled' in request.form,
|
||||
})
|
||||
errors = validate.validate_config(cfg)
|
||||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
changes = diff_fields(before, res)
|
||||
flash(record_group(cfg, 'dhcp_reservations', 'mac', mac, changes, 'core apply'), 'success')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/dhcpreservations/reservations_delete', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def reservations_delete():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('dhcp_reservations', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
removed = items.pop(idx)
|
||||
errors = validate.validate_config(cfg)
|
||||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
changes = diff_fields(removed, None)
|
||||
flash(record_group(cfg, 'dhcp_reservations', 'mac', removed['mac'], changes, 'core apply'), 'success')
|
||||
return redirect(f'/{_PAGE}')
|
||||
206
docker/routlin-dash/app/pages/dhcpreservations/content.json
Normal file
206
docker/routlin-dash/app/pages/dhcpreservations/content.json
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
{
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{
|
||||
"type": "header_page_title",
|
||||
"items": [
|
||||
{
|
||||
"type": "h1",
|
||||
"text": "DHCP Reservations"
|
||||
},
|
||||
{
|
||||
"type": "p",
|
||||
"text": "IP reservations and VLAN authorizations."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "table",
|
||||
"datasource": "config:dhcp_reservations",
|
||||
"empty_message": "No DHCP reservations configured.",
|
||||
"toolbar": {
|
||||
"items": [
|
||||
{
|
||||
"type": "select",
|
||||
"name": "vlan_filter",
|
||||
"value": "all",
|
||||
"options": "%VLAN_FILTER_OPTIONS%",
|
||||
"filter_col": "vlan_name"
|
||||
}
|
||||
]
|
||||
},
|
||||
"columns": [
|
||||
{
|
||||
"label": "Description",
|
||||
"field": "description"
|
||||
},
|
||||
{
|
||||
"label": "Hostname",
|
||||
"field": "hostname",
|
||||
"class": "col-mono"
|
||||
},
|
||||
{
|
||||
"label": "MAC",
|
||||
"field": "mac",
|
||||
"class": "col-mono"
|
||||
},
|
||||
{
|
||||
"label": "IP",
|
||||
"field": "ip",
|
||||
"class": "col-mono"
|
||||
},
|
||||
{
|
||||
"label": "VLAN",
|
||||
"field": "vlan_name"
|
||||
},
|
||||
{
|
||||
"label": "RADIUS",
|
||||
"field": "radius_client",
|
||||
"render": "badge_yes_no"
|
||||
},
|
||||
{
|
||||
"label": "Status",
|
||||
"field": "enabled",
|
||||
"render": "badge_enabled_disabled"
|
||||
}
|
||||
],
|
||||
"row_actions": [
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/dhcpreservations/reservations_edit",
|
||||
"method": "inline_edit",
|
||||
"text": "Edit",
|
||||
"class": "btn-ghost btn-sm",
|
||||
"fields": [
|
||||
{
|
||||
"col": "description",
|
||||
"input_type": "text"
|
||||
},
|
||||
{
|
||||
"col": "hostname",
|
||||
"input_type": "text",
|
||||
"validate": "VALIDATION_NETWORK_NAME"
|
||||
},
|
||||
{
|
||||
"col": "mac",
|
||||
"input_type": "text",
|
||||
"validate": "VALIDATION_MAC"
|
||||
},
|
||||
{
|
||||
"col": "ip",
|
||||
"input_type": "text",
|
||||
"validate": "VALIDATION_IPV4_FORMAT|VALIDATION_ADDRESS",
|
||||
"attrs": {
|
||||
"data-vlan-subnets": "%VLAN_SUBNET_INFO_JSON%"
|
||||
}
|
||||
},
|
||||
{
|
||||
"col": "radius_client",
|
||||
"input_type": "checkbox",
|
||||
"checkbox_label": "Enabled"
|
||||
},
|
||||
{
|
||||
"col": "enabled",
|
||||
"input_type": "checkbox",
|
||||
"checkbox_label": "Enabled"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/dhcpreservations/reservations_delete",
|
||||
"method": "post",
|
||||
"text": "Delete",
|
||||
"class": "btn-danger btn-sm"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "card",
|
||||
"id": "add-form",
|
||||
"label": "Add Reservation/Authorization",
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/dhcpreservations/addreservation_add",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
"type": "field",
|
||||
"label": "VLAN",
|
||||
"name": "vlan_name",
|
||||
"input_type": "select",
|
||||
"options": "%VLAN_NAMES_AS_OPTIONS%",
|
||||
"hint": "VLAN this reservation belongs to."
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Description",
|
||||
"name": "description",
|
||||
"input_type": "text",
|
||||
"placeholder": "e.g. NAS"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Hostname",
|
||||
"name": "hostname",
|
||||
"input_type": "text",
|
||||
"validate": "VALIDATION_NETWORK_NAME",
|
||||
"optional": true,
|
||||
"placeholder": "e.g. nas",
|
||||
"attrs": {
|
||||
"data-res-hosts-by-vlan": "%RESERVATION_HOSTNAMES_BY_VLAN_JSON%"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "MAC Address",
|
||||
"name": "mac",
|
||||
"input_type": "text",
|
||||
"validate": "VALIDATION_MAC",
|
||||
"placeholder": "e.g. aa:bb:cc:dd:ee:ff"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "IP Address",
|
||||
"name": "ip",
|
||||
"input_type": "text",
|
||||
"validate": "VALIDATION_IPV4_FORMAT|VALIDATION_ADDRESS",
|
||||
"optional": true,
|
||||
"placeholder": "e.g. 192.168.10.50",
|
||||
"hint": "Leave blank to authorize device on this VLAN dynamically.",
|
||||
"attrs": {
|
||||
"data-res-ips-by-vlan": "%RESERVATION_IPS_BY_VLAN_JSON%",
|
||||
"data-vlan-subnets": "%VLAN_SUBNET_INFO_JSON%",
|
||||
"data-vlan-select": "vlan_name"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "RADIUS Client",
|
||||
"name": "radius_client",
|
||||
"input_type": "checkbox",
|
||||
"hint": "This device acts as a RADIUS authenticator, verifying credentials of other devices on the network."
|
||||
},
|
||||
{
|
||||
"type": "button_row",
|
||||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/dhcpreservations/addreservation_add",
|
||||
"method": "post",
|
||||
"text": "Add"
|
||||
},
|
||||
{
|
||||
"type": "button_cancel",
|
||||
"text": "Cancel"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue