]> git.giorgioravera.it Git - network-manager.git/commitdiff
Added host saving in BIND9 zone format and JSON output
authorGiorgio Ravera <giorgio.ravera@gmail.com>
Sat, 31 Jan 2026 12:05:09 +0000 (13:05 +0100)
committerGiorgio Ravera <giorgio.ravera@gmail.com>
Sat, 31 Jan 2026 12:05:09 +0000 (13:05 +0100)
README.md
backend/db/hosts.py
backend/main.py
backend/routes/dns.py
docker-compose.yaml
frontend/js/hosts.js
settings/default.py
settings/settings.py

index 31999b92d0e6e2fb900395160947dc092799bc03..4d639e9cb3dff6f0d274da52b97b34b865c81777 100644 (file)
--- a/README.md
+++ b/README.md
@@ -127,21 +127,25 @@ secrets:
 | Variable | Default | Description |
 |----------|---------|-------------|
 | `FRONTEND_DIR` | /app/frontend | Frontend directory |
-| `DB_FILE` | /data/database.db |  SQLite file |
-| `DB_RESET` | false |  Reset DB on every startup |
-| `LOG_LEVEL` | info |  Log level |
-| `LOG_TO_FILE` | false |  Enable file logging |
-| `LOG_FILE` | /data/app.log |  Application log file |
-| `LOG_ACCESS_FILE` | /data/access.log |  HTTP access log |
-| `DOMAIN` | example.com |  Public domain |
-| `PUBLIC_IP` | 127.0.0.1 |  Public IP |
-| `HTTP_PORT` | 8000 |  Internal HTTP port |
-| `LOGIN_MAX_ATTEMPTS` | 5 |  Login attempts |
-| `LOGIN_WINDOW_SECONDS` | 600 |  Attempt window |
+| `DB_FILE` | /data/database.db | SQLite file |
+| `DB_RESET` | false | Reset DB on every startup |
+| `LOG_LEVEL` | info | Log level |
+| `LOG_TO_FILE` | false | Enable file logging |
+| `LOG_FILE` | /data/app.log | Application log file |
+| `LOG_ACCESS_FILE` | /data/access.log | HTTP access log |
+| `DOMAIN` | example.com | Public domain |
+| `PUBLIC_IP` | 127.0.0.1 | Public IP |
+| `HTTP_PORT` | 8000 | Internal HTTP port |
+| `LOGIN_MAX_ATTEMPTS` | 5 | Login attempts |
+| `LOGIN_WINDOW_SECONDS` | 600 | Attempt window |
 | `ADMIN_USER` | admin |  Admin username |
-| `ADMIN_PASSWORD` | admin |  Admin password (development) |
+| `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 |
 
 ---
 
index aacc09d92c55d30069b2cd6fb5f3828bb320c090..df6889849e3e6d1308087a9f5831e43559615e55 100644 (file)
@@ -16,9 +16,11 @@ from log.log import get_logger
 # -----------------------------
 def get_hosts():
     conn = get_db()
-    cur = conn.execute("SELECT * FROM hosts ORDER BY name")
-    rows = cur.fetchall()
-    return [dict(r) for r in rows]
+    cur = conn.execute("SELECT * FROM hosts")
+    rows = [dict(r) for r in cur.fetchall()]
+    # Sort by IPv4
+    rows.sort(key=lambda h: ipaddress.IPv4Address(h['ipv4']))
+    return rows
 
 # -----------------------------
 # SELECT SINGLE HOST
index 6cc004716d459e5e96754594fd88da6d72770d3a..bb0fcadb97e1943d5a3e0c08bacad163c95c7434 100644 (file)
@@ -55,6 +55,10 @@ def print_welcome():
         "Users: admin=%s | password=%s | hash=%s | hash_file=%s",
         settings.ADMIN_USER, safe_admin_pwd, safe_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
+    )
 
 # ------------------------------------------------------------------------------
 # Shutdown log
index fb133a7708a91af5668d1281c22f9d752c188005..c81d67569c413f185b1305c9ba5c5edaa9f71e01 100644 (file)
@@ -2,12 +2,14 @@
 
 # import standard modules
 from fastapi import APIRouter, Request, Response
-from fastapi.responses import FileResponse, RedirectResponse
+from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
 import asyncio
+import json
 import os
 import ipaddress
 import time
-
+# Import local modules
+from backend.db.hosts import get_hosts
 # Import Settings
 from settings.settings import settings
 
@@ -21,16 +23,54 @@ router = APIRouter()
 async def apt_dns_reload(request: Request):
     start_ns = time.monotonic_ns()
 
-    await asyncio.sleep(0.2)
+    # Inizializzazioni
+    error = False
+    message = None
+    code = None
+    status = None
+
+    try:
+        # Get Hosts List
+        hosts = get_hosts()
+
+        # Save DNS Configuration
+        path = settings.DNS_HOST_FILE
+        with open(path, "w", encoding="utf-8") as f:
+            for h in hosts:
+                line = f"{h.get('name')}\t\t IN\tA\t{h.get('ipv4')}\n"
+                f.write(line)
+
+        path = settings.DNS_HOST_FILE + ".json"
+        with open(path, "w", encoding="utf-8") as f:
+            #json.dump(hosts, f, indent=4, ensure_ascii=False)
+            for h in hosts:
+                f.write(json.dumps(h, ensure_ascii=False) + "\n")
+
+    except Exception as err:
+        error = True
+        message = str(err).strip()
+
+    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
 
-    end_ns = time.monotonic_ns()
-    took_ms = (end_ns - start_ns)
+    took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
 
-    return {
-        "code": "DNS_RELOAD_OK",
-        "status": "success",
-        "message": "DNS configuration reload successfully",
+    payload = {
+        "code": code,
+        "status": status,
+        "message": message,
         "details": {
             "took_ms": took_ms
         }
     }
+    return JSONResponse(content=payload)
index 3c7a1bf0fce08b7bb7dd67665c72a5900e03906c..d2d923be8a05bd15802ebabf37b75e8a76ad9192 100644 (file)
@@ -9,6 +9,7 @@ services:
       - TZ=${DOCKER_TZ}
     volumes:
       - ${DOCKER_CFG_DIR}/network:/data
+      - ${DOCKER_CFG_DIR}/bind9:/dns
     networks:
       - proxy
 
index 28d4ed43c13313acc3498ffee07963fc76cbb207..e71daf1167be31b72969a1854aa4afbc3139b498 100644 (file)
@@ -432,7 +432,7 @@ async function handleReloadDNS() {
 
         const data = await res.json();
         if(data.code !== "DNS_RELOAD_OK"){
-            const err = new Error(`Error reloading DNS: ${data.code} ${data.message}`);
+            const err = new Error(`Error reloading DNS: ${data.message}`);
             err.status = data.code;
             throw err;
         }
@@ -465,7 +465,7 @@ async function handleReloadDHCP() {
 
         const data = await res.json();
         if(data.code !== "DHCP_RELOAD_OK"){
-            const err = new Error(`Error reloading DHCP: ${data.code} ${data.message}`);
+            const err = new Error(`Error reloading DHCP: ${data.message}`);
             err.status = data.code;
             throw err;
         }
index 1013cad25e1e825fa2faee2be657afd939c4e2a3..dc9e9603882a66c114be85d7ed310aeb028fda50 100644 (file)
@@ -38,3 +38,11 @@ LOGIN_WINDOW_SECONDS = "600"
 ADMIN_USER = "admin"
 ADMIN_PASSWORD = "admin"
 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"
index 76e6a93663d0f7228ddfb094978e30109da6ddc1..351de22dd86fd10fa402de272608fc4372a5c0ef 100644 (file)
@@ -84,6 +84,12 @@ class Settings(BaseModel):
         (os.getenv("ADMIN_PASSWORD_HASH") or _read_text_if_exists(os.getenv("ADMIN_PASSWORD_HASH_FILE", default.ADMIN_PASSWORD_HASH_FILE)) or None)
     ))
 
+    # 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))
+
     def model_post_init(self, __context) -> None:
         if self.DEVEL:
             ts = datetime.datetime.now().strftime("%Y%m%d-%H%M")
@@ -91,6 +97,14 @@ class Settings(BaseModel):
         else:
             object.__setattr__(self, "APP_VERSION", self.APP_VERSION)
 
+        # Update Files
+        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
 # ---------------------------------------------------------
 # Singleton
 # ---------------------------------------------------------