Files
OPNexus/backend/app/routers/network.py
T
Julian Boldt b341ccd49f
Release / verify (push) Canceled after 0s
CI / checks (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / backend (push) Canceled after 0s
Release / test (push) Canceled after 0s
Release / publish (backend) (push) Canceled after 0s
Release / publish (frontend) (push) Canceled after 0s
Release / publish (proxy) (push) Canceled after 0s
Release v1.13.0
Mehr Verwaltung pro Firewall und ein Abgleich der Cluster-Knoten. Alles über
dieselbe abgesicherte Engine wie in 1.12.0 und live auf drei Test-Firewalls
geprüft, siehe Handbuch „Erweiterung 1.13.0“:

- **Netzwerk › Geräte:** LAGG, Bridge, Loopback und GRE. Dazu Gateway-Gruppen,
  DNS-Weiterleitungen (Unbound) und DHCP-Bereiche (dnsmasq).
- **Neue Gruppe „Host-System“:** Tunables (immer mit Bestätigung, Vorgaben nur per
  „Überschreiben“), Cron, Syslog-Ziele und ZFS-Snapshots (anlegen und löschen; der
  aktive Snapshot ist geschützt, das Aktivieren bleibt OPNsense selbst überlassen).
- **Netzwerk › Abgleich:** zeigt Unterschiede in der Netzwerk-Konfiguration zwischen den
  Knoten eines Clusters. Neuer Alarm „Netzwerk-Abweichung im Cluster“ (Einstellungen ›
  Benachrichtigungen, Migration 0004).
- **Auf den Partner übertragen:** Nach dem Anlegen eines Objekts lässt es sich mit
  einem Klick auch auf dem anderen Cluster-Knoten anlegen (eigene Vorschau, eigenes Backup).
- **Kopfzeile:** Mit acht Gruppen zeigt die Navigation die Namen ab 1150 px, darunter
  nur Icons, damit Suche und Abmelden in einer Zeile bleiben.
2026-10-09 22:08:39 +02:00

91 lines
3.8 KiB
Python

import re
import uuid
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.errors import ErrorCode, api_error
from app.models.host import OpnsenseHost
from app.routers.hosts import _upstream_error
from app.schemas.network import (
HostNetwork, NetworkChangeExecute, NetworkChangePreview, NetworkChangeRequest, NetworkChangeResult,
)
from app.services import network_change_service, network_service
from app.services.policy_service import policy_lock
from app.services.opnsense_client import OpnsenseAPIError
router = APIRouter(prefix="/hosts/{host_id}/network", tags=["network"])
@router.get("", response_model=HostNetwork)
async def host_network(host_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
host = await db.get(OpnsenseHost, host_id)
if not host:
raise api_error(404, ErrorCode.HOST_NICHT_GEFUNDEN)
try:
return await network_service.get_host_network(host)
except OpnsenseAPIError as exc:
raise _upstream_error(host, exc) from exc
async def _host_or_404(db: AsyncSession, host_id: uuid.UUID) -> OpnsenseHost:
host = await db.get(OpnsenseHost, host_id)
if not host:
raise api_error(404, ErrorCode.HOST_NICHT_GEFUNDEN)
return host
@router.post("/changes/preview", response_model=NetworkChangePreview)
async def preview_change(host_id: uuid.UUID, payload: NetworkChangeRequest, db: AsyncSession = Depends(get_db)):
host = await _host_or_404(db, host_id)
try:
plan = await network_change_service.preview(db, host, payload.kind, payload.action, payload.uuid, payload.fields)
except OpnsenseAPIError as exc:
raise _upstream_error(host, exc) from exc
return {k: v for k, v in plan.items() if not k.startswith("_")}
@router.post("/changes", response_model=NetworkChangeResult)
async def execute_change(host_id: uuid.UUID, payload: NetworkChangeExecute, db: AsyncSession = Depends(get_db)):
await policy_lock(db)
host = await _host_or_404(db, host_id)
return await network_change_service.execute(
db, host, payload.kind, payload.action, payload.uuid, payload.fields, payload.preview_token, payload.acknowledge)
@router.get("/template/{kind}")
async def change_template(host_id: uuid.UUID, kind: str, db: AsyncSession = Depends(get_db)):
"""Blank form for one kind, with the choices this firewall accepts for its select fields."""
if kind not in network_change_service.FIELDS:
raise api_error(404, ErrorCode.NETWORK_ITEM_NOT_FOUND)
host = await _host_or_404(db, host_id)
try:
return await network_change_service.template(host, kind)
except OpnsenseAPIError as exc:
raise _upstream_error(host, exc) from exc
@router.get("/item/{kind}/{item_uuid}")
async def current_item(host_id: uuid.UUID, kind: str, item_uuid: str, db: AsyncSession = Depends(get_db)):
"""The object's current values on the firewall, for pre-filling an edit form."""
if kind not in network_change_service.FIELDS or not re.fullmatch(r"[A-Za-z0-9_.:-]{1,64}", item_uuid):
raise api_error(404, ErrorCode.NETWORK_ITEM_NOT_FOUND)
host = await _host_or_404(db, host_id)
try:
return await network_change_service.item(host, kind, item_uuid)
except OpnsenseAPIError as exc:
raise _upstream_error(host, exc) from exc
@router.get("/objects/{kind}")
async def list_objects(host_id: uuid.UUID, kind: str, db: AsyncSession = Depends(get_db)):
"""Rows of a generic object kind (tunables, cron jobs, LAGG, ...), reduced to their display columns."""
if kind not in network_change_service.OBJECT_COLUMNS:
raise api_error(404, ErrorCode.NETWORK_ITEM_NOT_FOUND)
host = await _host_or_404(db, host_id)
try:
return await network_change_service.list_objects(host, kind)
except OpnsenseAPIError as exc:
raise _upstream_error(host, exc) from exc