import copy import ipaddress from flask import Blueprint, request, redirect, flash from auth import require_level from config_utils import load_config, save_config_with_snapshot, verify_config_hash import sanitize import validation as validate bp = Blueprint('action_apply_host_overrides', __name__) VIEW = '/view/view_host_overrides' def _vlan_networks(cfg): nets = [] for v in cfg.get('vlans', []): subnet = v.get('subnet', '') mask = v.get('subnet_mask', '') if subnet and mask: try: nets.append(ipaddress.IPv4Network(f'{subnet}/{mask}', strict=False)) except ValueError: pass return nets def _ip_in_vlan(ip_str, cfg): try: addr = ipaddress.IPv4Address(ip_str) except ValueError: return False nets = _vlan_networks(cfg) return not nets or any(addr in net for net in nets) 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 @bp.route('/action/add_host_override', methods=['POST']) @require_level('administrator') def add_host_override(): description = sanitize.text(request.form.get('description', '')) host = validate.domainname(request.form.get('host', '')) ip = sanitize.ip(request.form.get('ip', '')) if not host or not ip: flash('Hostname and IP address are required.', 'error') return redirect(VIEW) if not _hash_ok(): return redirect(VIEW) cfg = load_config() if not _ip_in_vlan(ip, cfg): flash('IP address does not fall within any configured VLAN subnet.', 'error') return redirect(VIEW) entry = {'description': description, 'host': host, 'ip': ip, 'enabled': True} cfg.setdefault('host_overrides', []).append(entry) errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) flash(save_config_with_snapshot( cfg, path='host_overrides', key=host, operation='add', before=None, after=entry, description=f'Added host override: {host} → {ip}', ), 'success') return redirect(VIEW) @bp.route('/action/toggle_host_override', methods=['POST']) @require_level('administrator') def toggle_host_override(): idx = _row_index() if idx is None: flash('Invalid request.', 'error') return redirect(VIEW) if not _hash_ok(): return redirect(VIEW) cfg = load_config() items = cfg.get('host_overrides', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) old_enabled = items[idx].get('enabled', True) items[idx]['enabled'] = not old_enabled errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) action = 'Enabled' if not old_enabled else 'Disabled' flash(save_config_with_snapshot( cfg, path='host_overrides', key=items[idx]['host'], operation='toggle', before={'enabled': old_enabled}, after={'enabled': not old_enabled}, description=f'{action} host override: {items[idx]["host"]}', ), 'success') return redirect(VIEW) @bp.route('/action/edit_host_override', methods=['POST']) @require_level('administrator') def edit_host_override(): idx = _row_index() if idx is None: flash('Invalid request.', 'error') return redirect(VIEW) description = sanitize.text(request.form.get('description', '')) host = validate.domainname(request.form.get('host', '')) ip = sanitize.ip(request.form.get('ip', '')) enabled = request.form.get('enabled') == 'on' if not host or not ip: flash('Hostname and IP address are required.', 'error') return redirect(VIEW) if not _hash_ok(): return redirect(VIEW) cfg = load_config() if not _ip_in_vlan(ip, cfg): flash('IP address does not fall within any configured VLAN subnet.', 'error') return redirect(VIEW) items = cfg.get('host_overrides', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) before = copy.deepcopy(items[idx]) items[idx].update({'description': description, 'host': host, 'ip': ip, 'enabled': enabled}) errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) flash(save_config_with_snapshot( cfg, path='host_overrides', key=host, operation='edit', before=before, after=copy.deepcopy(items[idx]), description=f'Edited host override: {host} → {ip}', ), 'success') return redirect(VIEW) @bp.route('/action/delete_host_override', methods=['POST']) @require_level('administrator') def delete_host_override(): idx = _row_index() if idx is None: flash('Invalid request.', 'error') return redirect(VIEW) if not _hash_ok(): return redirect(VIEW) cfg = load_config() items = cfg.get('host_overrides', []) if idx < 0 or idx >= len(items): flash('Entry not found.', 'error') return redirect(VIEW) removed = items.pop(idx) errors = validate.validate_config(cfg) if errors: for msg in errors: flash(msg, 'error') return redirect(VIEW) flash(save_config_with_snapshot( cfg, path='host_overrides', key=removed['host'], operation='delete', before=removed, after=None, description=f'Deleted host override: {removed["host"]}', ), 'success') return redirect(VIEW)