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:
| `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 |
---
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
)
# ------------------------------------------------------------------------------
# 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
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
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 = []
# 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)
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
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)
// -----------------------------
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;
// -----------------------------
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;
# ---------------------------------------------------------
# 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"
))
# 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))
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