From 6c35874f4b985853a1f60886684c544f6e61a662 Mon Sep 17 00:00:00 2001 From: Giorgio Ravera Date: Mon, 2 Feb 2026 21:48:17 +0100 Subject: [PATCH] Added DNS reverse & DHCP Configuration & Backup --- README.md | 10 ++++-- TODO.md | 8 ++++- backend/main.py | 6 ++++ backend/routes/backup.py | 72 +++++++++++++++++++++++++++++++++++++++ backend/routes/dhcp.py | 73 +++++++++++++++++++++++++++++++++++----- backend/routes/dns.py | 14 +++++--- docker-compose.yaml | 1 + settings/default.py | 18 ++++++++-- settings/settings.py | 19 ++++++++++- 9 files changed, 200 insertions(+), 21 deletions(-) create mode 100644 backend/routes/backup.py diff --git a/README.md b/README.md index 4d639e9..4a81e73 100644 --- a/README.md +++ b/README.md @@ -127,12 +127,13 @@ secrets: | Variable | Default | Description | |----------|---------|-------------| | `FRONTEND_DIR` | /app/frontend | Frontend directory | -| `DB_FILE` | /data/database.db | SQLite file | +| `DATA_PATH`| /data | Data Path for DB and Backups | +| `DB_FILE` | 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 | +| `LOG_FILE` | app.log | Application log file | +| `LOG_ACCESS_FILE` | access.log | HTTP access log | | `DOMAIN` | example.com | Public domain | | `PUBLIC_IP` | 127.0.0.1 | Public IP | | `HTTP_PORT` | 8000 | Internal HTTP port | @@ -146,6 +147,9 @@ secrets: | `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 | +| `DHCP6_HOST_FILE` | hosts-ipv6.json | KEA-DHCP6 Hosts file | --- diff --git a/TODO.md b/TODO.md index f6b8a92..4aadeef 100644 --- a/TODO.md +++ b/TODO.md @@ -22,7 +22,13 @@ - [ ] Perform **commit + push** on Git - [ ] Regenerate **from scratch**: - [ ] **BIND (DNS)** configuration - - [ ] **Kea (DHCP)** configuration + - [X] hosts configuration + - [ ] alias configuration + - [X] reverse configuration + - [ ] IPv6 configuration + - [X] **Kea (DHCP)** configuration + - [X] IPv4 configuration + - [X] IPv6 configuration - [ ] Reload services: - [ ] BIND - [ ] Kea diff --git a/backend/main.py b/backend/main.py index bb0fcad..20f971c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -9,6 +9,7 @@ import logging import os # Import Routers from backend.routes.about import router as about_router +from backend.routes.backup import router as backup_router from backend.routes.health import router as health_router from backend.routes.login import router as login_router from backend.routes.hosts import router as hosts_router @@ -59,6 +60,10 @@ def print_welcome(): "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 ) + 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 + ) # ------------------------------------------------------------------------------ # Shutdown log @@ -98,6 +103,7 @@ app = FastAPI( # Routers # ------------------------------------------------------------------------------ app.include_router(about_router) +app.include_router(backup_router) app.include_router(health_router) app.include_router(login_router) app.include_router(hosts_router) diff --git a/backend/routes/backup.py b/backend/routes/backup.py new file mode 100644 index 0000000..e2d40b5 --- /dev/null +++ b/backend/routes/backup.py @@ -0,0 +1,72 @@ +# backend/routes/backup.py + +# import standard modules +from fastapi import APIRouter, Request, Response +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 + +# Create Router +router = APIRouter() + +# --------------------------------------------------------- +# API ENDPOINTS +# --------------------------------------------------------- +@router.get("/api/backup") +async def apt_dns_reload(request: Request): + start_ns = time.monotonic_ns() + + # Inizializzazioni + error = False + message = None + code = None + status = None + dns_hosts = [] + dns_reverse = [] + + try: + # Get Hosts List + hosts = get_hosts() + + # Backup Hosts DB + path = settings.DATA_PATH + "/hosts.json" + with open(path, "w", encoding="utf-8") as f: + 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 = "BACKUP_ERROR" + # default del messaggio se vuoto o None + if not message: + message = "BACKUP error" + status = "failure" + #http_status = 500 + else: + code = "BACKUP_OK" + message = "BACKUP executed 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 + } + } + return JSONResponse(content=payload) + #return JSONResponse(content=payload, status_code=http_status) diff --git a/backend/routes/dhcp.py b/backend/routes/dhcp.py index 37f0b5f..e9c4c52 100644 --- a/backend/routes/dhcp.py +++ b/backend/routes/dhcp.py @@ -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,69 @@ router = APIRouter() async def apt_dhcp_reload(request: Request): start_ns = time.monotonic_ns() - await asyncio.sleep(0.2) + # Inizializzazioni + error = False + message = None + code = None + status = None + kea4_hosts = [] + kea6_hosts = [] + + try: + # Get Hosts List + hosts = get_hosts() + + # Convert hosts into the kea structure + for h in hosts: + if h.get("ipv4") and h.get("mac"): + kea4_hosts.append({ + "hostname": h.get("name"), + "hw-address": h.get("mac"), + "ip-address": h.get("ipv4"), + }) + if h.get("ipv6") and h.get("mac"): + kea6_hosts.append({ + "hostname": h.get("name"), + "hw-address": h.get("mac"), + "ip-address": h.get("ipv6"), + }) + + # Save DHCP4 Configuration + path = settings.DHCP4_HOST_FILE + with open(path, "w", encoding="utf-8") as f: + json.dump(kea4_hosts, f, indent=4, ensure_ascii=False) + + # Save DHCP6 Configuration + path = settings.DHCP6_HOST_FILE + with open(path, "w", encoding="utf-8") as f: + json.dump(kea6_hosts, f, indent=4, ensure_ascii=False) + + except Exception as err: + 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 - end_ns = time.monotonic_ns() - took_ms = (end_ns - start_ns) + took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 - return { - "code": "DHCP_RELOAD_OK", - "status": "success", - "message": "DHCP configuration reload successfully", + payload = { + "code": code, + "status": status, + "message": message, "details": { "took_ms": took_ms } } + return JSONResponse(content=payload) + #return JSONResponse(content=payload, status_code=http_status) diff --git a/backend/routes/dns.py b/backend/routes/dns.py index c81d675..92bde01 100644 --- a/backend/routes/dns.py +++ b/backend/routes/dns.py @@ -33,18 +33,23 @@ async def apt_dns_reload(request: Request): # Get Hosts List hosts = get_hosts() - # Save DNS Configuration + # Save DNS Hosts 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" + # Save DNS Reverse Configuration + path = settings.DNS_REVERSE_FILE 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") + 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: error = True @@ -74,3 +79,4 @@ async def apt_dns_reload(request: Request): } } return JSONResponse(content=payload) + #return JSONResponse(content=payload, status_code=http_status) diff --git a/docker-compose.yaml b/docker-compose.yaml index d2d923b..0234f4c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,6 +10,7 @@ services: volumes: - ${DOCKER_CFG_DIR}/network:/data - ${DOCKER_CFG_DIR}/bind9:/dns + - ${DOCKER_CFG_DIR}/kea:/dhcp networks: - proxy diff --git a/settings/default.py b/settings/default.py index dc9e960..8bddf08 100644 --- a/settings/default.py +++ b/settings/default.py @@ -5,10 +5,15 @@ # --------------------------------------------------------- FRONTEND_DIR = "/app/frontend" +# --------------------------------------------------------- +# Data Path (DB + Backup) +# --------------------------------------------------------- +DATA_PATH = "/data" + # --------------------------------------------------------- # Database # --------------------------------------------------------- -DB_FILE = "/data/database.db" +DB_FILE = "database.db" DB_RESET = False # --------------------------------------------------------- @@ -16,8 +21,8 @@ DB_RESET = False # --------------------------------------------------------- LOG_LEVEL = "INFO" LOG_TO_FILE = False -LOG_FILE = "/data/app.log" -LOG_ACCESS_FILE = "/data/access.log" +LOG_FILE = "app.log" +LOG_ACCESS_FILE = "access.log" # --------------------------------------------------------- # Host @@ -46,3 +51,10 @@ 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" + +# --------------------------------------------------------- +# DHCP +# --------------------------------------------------------- +DHCP_CFG_PATH="/dhcp/etc" +DHCP4_HOST_FILE="hosts-ipv4.json" +DHCP6_HOST_FILE="hosts-ipv6.json" diff --git a/settings/settings.py b/settings/settings.py index 351de22..c964b2f 100644 --- a/settings/settings.py +++ b/settings/settings.py @@ -51,6 +51,9 @@ class Settings(BaseModel): BASEIMG_NAME: str = Field(default_factory=lambda: config.BASEIMG_NAME) BASEIMG_VERSION: str = Field(default_factory=lambda: config.BASEIMG_VERSION) + # DATA_PATH + DATA_PATH: str = Field(default_factory=lambda: os.getenv("DATA_PATH", default.DATA_PATH)) + # Frontend FRONTEND_DIR: str = Field(default_factory=lambda: os.getenv("FRONTEND_DIR", default.FRONTEND_DIR)) @@ -89,6 +92,10 @@ class Settings(BaseModel): 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)) + DHCP6_HOST_FILE: str = Field(default_factory=lambda: os.getenv("DHCP6_HOST_FILE", default.DHCP6_HOST_FILE)) def model_post_init(self, __context) -> None: if self.DEVEL: @@ -97,7 +104,12 @@ class Settings(BaseModel): else: object.__setattr__(self, "APP_VERSION", self.APP_VERSION) - # Update Files + # Database + self.DB_FILE = self.DATA_PATH + "/" + self.DB_FILE + self.LOG_FILE = self.DATA_PATH + "/" + self.LOG_FILE + self.LOG_ACCESS_FILE = self.DATA_PATH + "/" + self.LOG_ACCESS_FILE + + # Update DNS 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) @@ -105,6 +117,11 @@ class Settings(BaseModel): 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.DHCP6_HOST_FILE = self.DHCP_CFG_PATH + "/" + self.DHCP6_HOST_FILE + # --------------------------------------------------------- # Singleton # --------------------------------------------------------- -- 2.47.3