]> git.giorgioravera.it Git - network-manager.git/commitdiff
Improved configuration and routers
authorGiorgio Ravera <giorgio.ravera@gmail.com>
Tue, 3 Feb 2026 22:13:42 +0000 (23:13 +0100)
committerGiorgio Ravera <giorgio.ravera@gmail.com>
Tue, 3 Feb 2026 22:13:42 +0000 (23:13 +0100)
.github/dependabot.yaml
README.md
backend/main.py
backend/routes/backup.py
backend/routes/dhcp.py
backend/routes/dns.py
frontend/js/hosts.js
settings/default.py
settings/settings.py

index cd4b98ecb11bf318976ae0380e7d6d90e46f7da3..c74b98786bc0f6d98879a4f9f8c032d846110fe7 100644 (file)
@@ -6,12 +6,6 @@ updates:
       interval: "weekly"
     open-pull-requests-limit: 10
 
-  - package-ecosystem: "npm"
-    directory: "/frontend"
-    schedule:
-      interval: "weekly"
-    open-pull-requests-limit: 10
-
   - package-ecosystem: "github-actions"
     directory: "/"
     schedule:
index 767f67ba6d7183b2395f90d20a5a2d0fcfd7cd41..81706611f0d20689d43ae8e10e1e09f4b9ffcd62 100644 (file)
--- a/README.md
+++ b/README.md
@@ -143,15 +143,13 @@ secrets:
 | `ADMIN_PASSWORD` | admin | Admin password (development) |
 | `ADMIN_PASSWORD_HASH_FILE` | /run/secrets/admin_password_hash |  Admin password hash |
 | `SESSION_SECRET` | (auto-generated) |  Session secret |
-| `DNS_CFG_PATH` | /dns/etc | Bind9 Configuration folder |
-| `DNS_HOST_FILE` | {DOMAIN}/hosts.inc | BIND9 Hosts file |
-| `DNS_ALIAS_FILE` | {DOMAIN}/alias.inc | BIND9 Alias file |
-| `DNS_REVERSE_FILE` | reverse/hosts.inc | BIND9 Reverse Hosts file |
-| `DHCP_CFG_PATH` | /dns/etc | KEA Configuration folder |
-| `DHCP4_HOST_FILE` | hosts-ipv4.json | KEA-DHCP4 Hosts file |
-| `DHCP4_LEASES_FILE` | dhcp4.leases | KEA-DHCP4 leases file |
-| `DHCP6_HOST_FILE` | hosts-ipv6.json | KEA-DHCP6 Hosts file |
-| `DHCP6_LEASES_FILE` | dhcp6.leases | KEA-DHCP6 leases file |
+| `DNS_HOST_FILE` | /dns/etc/{DOMAIN}/hosts.inc | BIND9 Hosts file |
+| `DNS_ALIAS_FILE` | /dns/etc/{DOMAIN}/alias.inc | BIND9 Alias file |
+| `DNS_REVERSE_FILE` | /dns/etc/reverse/hosts.inc | BIND9 Reverse Hosts file |
+| `DHCP4_HOST_FILE` | /dhcp/etc/hosts-ipv4.json | KEA-DHCP4 Hosts file |
+| `DHCP4_LEASES_FILE` | /dhcp/lib/dhcp4.leases | KEA-DHCP4 leases file |
+| `DHCP6_HOST_FILE` | /dhcp/etc/hosts-ipv6.json | KEA-DHCP6 Hosts file |
+| `DHCP6_LEASES_FILE` | /dhcp/lib/dhcp6.leases | KEA-DHCP6 leases file |
 
 ---
 
index d7b7cf548266a059a60aafa9fa2812de70f7bfcc..eeee80734e8b1c2ab60743c7312f129663f62edc 100644 (file)
@@ -57,12 +57,12 @@ def print_welcome():
         settings.ADMIN_USER, masked_admin_pwd, masked_admin_hash, settings.ADMIN_PASSWORD_HASH_FILE
     )
     logger.info(
-        "DNS: path=%s | host file=%s | alias file=%s | reverse file=%s",
-        settings.DNS_CFG_PATH, settings.DNS_HOST_FILE, settings.DNS_ALIAS_FILE, settings.DNS_REVERSE_FILE
+        "DNS: host file=%s | alias file=%s | reverse file=%s",
+        settings.DNS_HOST_FILE, settings.DNS_ALIAS_FILE, settings.DNS_REVERSE_FILE
     )
     logger.info(
-        "DHCP: path=%s | ipv4 host file=%s | ipv6 host file=%s",
-        settings.DHCP_CFG_PATH, settings.DHCP4_HOST_FILE, settings.DHCP6_HOST_FILE
+        "DHCP: ipv4 host file=%s | ipv4 leases file=%s | ipv6 host file=%s | ipv6 leases file=%s",
+        settings.DHCP4_HOST_FILE, settings.DHCP4_LEASES_FILE, settings.DHCP6_HOST_FILE, settings.DHCP6_LEASES_FILE
     )
 
 # ------------------------------------------------------------------------------
index e2d40b55e3a126b181f4786a922a3217883000bc..d0e2922e51bec3e4b4ccf4d248ba637bab17b8be 100644 (file)
@@ -20,7 +20,7 @@ router = APIRouter()
 # API ENDPOINTS
 # ---------------------------------------------------------
 @router.get("/api/backup")
-async def apt_dns_reload(request: Request):
+async def api_dns_reload(request: Request):
     start_ns = time.monotonic_ns()
 
     # Inizializzazioni
index 8287e93ed90429448fd2983f45f52b85030a1ae5..c1712bbd4e5039e0383852cd29921568de488b39 100644 (file)
@@ -4,9 +4,11 @@
 from fastapi import APIRouter, Request, Response
 from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
 import asyncio
+import csv
 import json
 import os
 import ipaddress
+from pathlib import Path
 import time
 # Import local modules
 from backend.db.hosts import get_hosts
@@ -19,17 +21,13 @@ from log.log import setup_logging, get_logger
 router = APIRouter()
 
 # ---------------------------------------------------------
-# API ENDPOINTS
+# Reload
 # ---------------------------------------------------------
-@router.get("/api/dhcp/reload")
-async def apt_dhcp_reload(request: Request):
-    start_ns = time.monotonic_ns()
+@router.post("/api/dhcp/reload")
+async def api_dhcp_reload(request: Request):
 
     # Inizializzazioni
-    error = False
-    message = None
-    code = None
-    status = None
+    start_ns = time.monotonic_ns()
     kea4_hosts = []
     kea6_hosts = []
 
@@ -54,43 +52,126 @@ async def apt_dhcp_reload(request: Request):
 
         # Save DHCP4 Configuration
         path = settings.DHCP4_HOST_FILE
-        data = {"hosts": kea4_hosts}
+        data = {"reservations": kea4_hosts}
+        full = json.dumps(data, indent=4, ensure_ascii=False)
+        fragment = full.strip()[1:-1].strip() + "\n"
         with open(path, "w", encoding="utf-8") as f:
-            json.dump(data, f, indent=4, ensure_ascii=False)
+            f.write(fragment)
 
         # Save DHCP6 Configuration
         path = settings.DHCP6_HOST_FILE
-        data = {"hosts": kea6_hosts}
+        data = {"reservations": kea6_hosts}
+        full = json.dumps(data, indent=4, ensure_ascii=False)
+        fragment = full.strip()[1:-1].strip() + "\n"
         with open(path, "w", encoding="utf-8") as f:
-            json.dump(data, f, indent=4, ensure_ascii=False)
+            f.write(fragment)
+
+        # RELOAD DHCP
+
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+        payload = {
+            "code": "DHCP_RELOAD_OK",
+            "status": "success",
+            "message": "DHCP configuration reload successfully",
+            "details": {"took_ms": took_ms}
+        }
+        return JSONResponse(content=payload, status_code=200)
+
+    except Exception as err:
+        get_logger("dhcp").exception("Error reloading DHCP: %s", str(err).strip())
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+
+        payload = {
+            "code": "DHCP_RELOAD_ERROR",
+            "status": "failure",
+            "message": "Error reloading DHCP",
+            "details": {"took_ms": took_ms, "error": str(err).strip()}
+        }
+        return JSONResponse(content=payload, status_code=500)
+
+# ---------------------------------------------------------
+# Get Leases
+# ---------------------------------------------------------
+@router.get("/api/dhcp/leases")
+def api_dhcp_leases(request: Request):
+
+    # Inizializzazioni
+    items = []
+
+    try:
+        path = Path(settings.DHCP4_LEASES_FILE)
+        if not path.exists():
+            return JSONResponse(
+                content={"code": "DHCP_LEASES_NOT_FOUND",
+                         "status": "failure",
+                         "message": f"DHCP leases file not found: {str(path)}",
+                         "details": {}},
+                status_code=404
+            )
+
+        def _to_int(v: str):
+            v = (v or "").strip()
+            if not v or v.lower() == "null":
+                return None
+            try:
+                return int(v)
+            except ValueError:
+                return None
+
+        def _to_bool(v: str):
+            v = (v or "").strip().lower()
+            if v in ("true", "1", "yes", "y"):
+                return True
+            if v in ("false", "0", "no", "n"):
+                return False
+            return None
+
+        def _norm(col: str) -> str:
+            col = (col or "").strip()
+            aliases = {
+                "client_id": "client-id",
+                "valid_lifetime": "valid-lft",
+                "subnet_id": "subnet-id",
+                "fqdn_fwd": "fqdn-fwd",
+                "fqdn_rev": "fqdn-rev",
+                "user_context": "user-context",
+                "pool_id": "pool-id",
+            }
+            return aliases.get(col, col)
+
+        # Open the file in lettura (non locking): ok per kea memfile
+        with path.open("r", encoding="utf-8", newline="") as f:
+            reader = csv.DictReader(f)
+            if not reader.fieldnames:
+                return JSONResponse(content={"total": 0, "items": []}, status_code=200)
+
+            for raw in reader:
+                rec = { _norm(k): (v if v is not None else "") for k, v in raw.items() }
+
+                item = {
+                    "address":        rec.get("address", "").strip() or None,
+                    "hwaddr":         rec.get("hwaddr", "").strip() or None,
+                    "client_id":      rec.get("client-id", "").strip() or None,
+                    "valid_lifetime": _to_int(rec.get("valid-lft", "")),
+                    "expire":         _to_int(rec.get("expire", "")),        # epoch seconds
+                    "subnet_id":      _to_int(rec.get("subnet-id", "")),
+                    "fqdn_fwd":       _to_bool(rec.get("fqdn-fwd", "")),
+                    "fqdn_rev":       _to_bool(rec.get("fqdn-rev", "")),
+                    "hostname":       rec.get("hostname", "").strip() or None,
+                    "state":          _to_int(rec.get("state", "")),
+                    "user_context":   rec.get("user-context", "").strip() or None,  # spesso JSON serializzato
+                    "pool_id":        _to_int(rec.get("pool-id", "")),
+                }
+                items.append(item)
+
+        return JSONResponse(content={"total": len(items), "items": items}, status_code=200)
 
     except Exception as err:
-        get_logger("dhcp").exception("Error reloading DHCP: " + str(err).strip())
-        error = True
-        #message = str(err).strip()
-
-    if error:
-        code = "DHCP_RELOAD_ERROR"
-        # default del messaggio se vuoto o None
-        if not message:
-            message = "DHCP reload error"
-        status = "failure"
-        #http_status = 500
-    else:
-        code = "DHCP_RELOAD_OK"
-        message = "DHCP configuration reload successfully"
-        status = "success"
-        #http_status = 200
-
-    took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
-
-    payload = {
-        "code": code,
-        "status": status,
-        "message": message,
-        "details": {
-            "took_ms": took_ms
+        get_logger("dhcp").exception("Error reading DHCP leases: %s", str(err).strip())
+        payload = {
+            "code": "DHCP_LEASES_ERROR",
+            "status": "failure",
+            "message": "Error reading DHCP leases",
+            "details": {"error": str(err).strip()}
         }
-    }
-    return JSONResponse(content=payload)
-    #return JSONResponse(content=payload, status_code=http_status)
+        return JSONResponse(content=payload, status_code=500)
index f068551ef603b3023b97725b1d3073d6ee61c24c..c1174b5beeeb56d18cd284be1efb9dd4218aecd5 100644 (file)
@@ -19,17 +19,13 @@ from log.log import setup_logging, get_logger
 router = APIRouter()
 
 # ---------------------------------------------------------
-# API ENDPOINTS
+# Reload
 # ---------------------------------------------------------
-@router.get("/api/dns/reload")
-async def apt_dns_reload(request: Request):
-    start_ns = time.monotonic_ns()
+@router.post("/api/dns/reload")
+async def api_dns_reload(request: Request):
 
     # Inizializzazioni
-    error = False
-    message = None
-    code = None
-    status = None
+    start_ns = time.monotonic_ns()
 
     try:
         # Get Hosts List
@@ -46,40 +42,32 @@ async def apt_dns_reload(request: Request):
         path = settings.DNS_REVERSE_FILE
         with open(path, "w", encoding="utf-8") as f:
             for h in hosts:
-                ip = h.get('ipv4')
+                ip = h.get("ipv4")
                 if ip:
                     parts = ip.split(".")
                     rev = f"{parts[-1]}.{parts[-2]}"
                     line = f"{rev}\t\t IN PTR\t{h.get('name')}.{settings.DOMAIN}\n"
                     f.write(line)
 
-    except Exception as err:
-        get_logger("dns").exception("Error reloading DNS: " + str(err).strip())
-        error = True
-        #message = str(err).strip()
+        # RELOAD DNS
 
-    if error:
-        code = "DNS_RELOAD_ERROR"
-        # default del messaggio se vuoto o None
-        if not message:
-            message = "DNS reload error"
-        status = "failure"
-        #http_status = 500
-    else:
-        code = "DNS_RELOAD_OK"
-        message = "DNS configuration reload successfully"
-        status = "success"
-        #http_status = 200
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+        payload = {
+            "code": "DNS_RELOAD_OK",
+            "status": "success",
+            "message": "DNS configuration reload successfully",
+            "details": {"took_ms": took_ms}
+        }
+        return JSONResponse(content=payload, status_code=200)
 
-    took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
+    except Exception as err:
+        get_logger("dns").exception("Error reloading DNS: %s", str(err).strip())
+        took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
 
-    payload = {
-        "code": code,
-        "status": status,
-        "message": message,
-        "details": {
-            "took_ms": took_ms
+        payload = {
+            "code": "DNS_RELOAD_ERROR",
+            "status": "failure",
+            "message": "Error reloading DNS",
+            "details": {"took_ms": took_ms, "error": str(err).strip()}
         }
-    }
-    return JSONResponse(content=payload)
-    #return JSONResponse(content=payload, status_code=http_status)
+        return JSONResponse(content=payload, status_code=500)
index e71daf1167be31b72969a1854aa4afbc3139b498..ea82aac34426cb72a61ae5b3929c88d24db53633 100644 (file)
@@ -418,7 +418,7 @@ async function handleDeleteHost(e, el) {
 // -----------------------------
 async function handleReloadDNS() {
     try {
-        const res = await fetch(`/api/dns/reload`, { method: "GET" });
+        const res = await fetch(`/api/dns/reload`, { method: "POST" });
         if (!res.ok) {
             const err = new Error(`Error reloading DNS: ${res.status} ${res.statusText}`);
             err.status = res.status;
@@ -451,7 +451,7 @@ async function handleReloadDNS() {
 // -----------------------------
 async function handleReloadDHCP() {
     try {
-        const res = await fetch(`/api/dhcp/reload`, { method: "GET" });
+        const res = await fetch(`/api/dhcp/reload`, { method: "POST" });
         if (!res.ok) {
             const err = new Error(`Error reloading DHCP: ${res.status} ${res.statusText}`);
             err.status = res.status;
index 9bb6da2aa6cb3c24be8c35512aefbfc3b6060a95..7b013b4c8432c3d19d674cdb232583093254dd4a 100644 (file)
@@ -47,16 +47,14 @@ ADMIN_PASSWORD_HASH_FILE = "/run/secrets/admin_password_hash"
 # ---------------------------------------------------------
 # DNS
 # ---------------------------------------------------------
-DNS_CFG_PATH="/dns/etc"
-DNS_HOST_FILE=f"{DOMAIN}/hosts.inc"
-DNS_ALIAS_FILE=f"{DOMAIN}/alias.inc"
-DNS_REVERSE_FILE="reverse/hosts.inc"
+DNS_HOST_FILE=f"/dns/etc/{DOMAIN}/hosts.inc"
+DNS_ALIAS_FILE=f"/dns/etc/{DOMAIN}/alias.inc"
+DNS_REVERSE_FILE="/dns/etc/reverse/hosts.inc"
 
 # ---------------------------------------------------------
 # DHCP
 # ---------------------------------------------------------
-DHCP_CFG_PATH="/dhcp/etc"
-DHCP4_HOST_FILE="hosts-ipv4.json"
-DHCP4_LEASES_FILE="dhcp4.leases"
-DHCP6_HOST_FILE="hosts-ipv6.json"
-DHCP6_LEASES_FILE="dhcp6.leases"
+DHCP4_HOST_FILE="/dhcp/etc/hosts-ipv4.json"
+DHCP4_LEASES_FILE="/dhcp/lib/dhcp4.leases"
+DHCP6_HOST_FILE="/dhcp/etc/hosts-ipv6.json"
+DHCP6_LEASES_FILE="/dhcp/lib/dhcp6.leases"
index 62e2fd4658059b55983399e603aa17a46e10b00b..af8c5c2e262ca4a35752bed3ff0e595703fd7e5a 100644 (file)
@@ -88,12 +88,10 @@ class Settings(BaseModel):
     ))
 
     # DNS
-    DNS_CFG_PATH: str = Field(default_factory=lambda: os.getenv("DNS_CFG_PATH", default.DNS_CFG_PATH))
     DNS_HOST_FILE: str = Field(default_factory=lambda: os.getenv("DNS_HOST_FILE", default.DNS_HOST_FILE))
     DNS_ALIAS_FILE: str = Field(default_factory=lambda: os.getenv("DNS_ALIAS_FILE", default.DNS_ALIAS_FILE))
     DNS_REVERSE_FILE: str = Field(default_factory=lambda: os.getenv("DNS_REVERSE_FILE", default.DNS_REVERSE_FILE))
     # DHCP
-    DHCP_CFG_PATH: str = Field(default_factory=lambda: os.getenv("DHCP_CFG_PATH", default.DHCP_CFG_PATH))
     DHCP4_HOST_FILE: str = Field(default_factory=lambda: os.getenv("DHCP4_HOST_FILE", default.DHCP4_HOST_FILE))
     DHCP4_LEASES_FILE: str = Field(default_factory=lambda: os.getenv("DHCP4_LEASES_FILE", default.DHCP4_LEASES_FILE))
     DHCP6_HOST_FILE: str = Field(default_factory=lambda: os.getenv("DHCP6_HOST_FILE", default.DHCP6_HOST_FILE))
@@ -115,16 +113,6 @@ class Settings(BaseModel):
         if self.DOMAIN.lower() != default.DOMAIN.lower():
             self.DNS_HOST_FILE    = self.DNS_HOST_FILE.replace(default.DOMAIN, self.DOMAIN)
             self.DNS_ALIAS_FILE   = self.DNS_ALIAS_FILE.replace(default.DOMAIN, self.DOMAIN)
-            self.DNS_REVERSE_FILE = self.DNS_REVERSE_FILE.replace(default.DOMAIN, self.DOMAIN)
-        self.DNS_HOST_FILE    = self.DNS_CFG_PATH + "/" + self.DNS_HOST_FILE
-        self.DNS_ALIAS_FILE   = self.DNS_CFG_PATH + "/" + self.DNS_ALIAS_FILE
-        self.DNS_REVERSE_FILE = self.DNS_CFG_PATH + "/" + self.DNS_REVERSE_FILE
-
-        # Update DHCP Files
-        self.DHCP4_HOST_FILE    = self.DHCP_CFG_PATH + "/" + self.DHCP4_HOST_FILE
-        self.DHCP4_LEASES_FILE  = self.DHCP_CFG_PATH + "/" + self.DHCP4_LEASES_FILE
-        self.DHCP6_HOST_FILE    = self.DHCP_CFG_PATH + "/" + self.DHCP6_HOST_FILE
-        self.DHCP6_LEASES_FILE  = self.DHCP_CFG_PATH + "/" + self.DHCP6_LEASES_FILE
 
 # ---------------------------------------------------------
 # Singleton