From: Giorgio Ravera Date: Thu, 12 Mar 2026 16:36:00 +0000 (+0100) Subject: Updated log and settings module & created config table in db X-Git-Url: http://git.giorgioravera.it/?a=commitdiff_plain;h=c3e014fdd6026002f263237a8ddf19582fbaa9ee;p=network-manager.git Updated log and settings module & created config table in db --- diff --git a/Dockerfile b/Dockerfile index db0c261..c31cacb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,8 +15,6 @@ RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt # Copy full application COPY backend backend COPY frontend frontend -COPY log log -COPY settings settings # ---------- STAGE 2: Alpine Runtime ---------- FROM python:3.12-alpine diff --git a/backend/app.py b/backend/app.py index 41eef47..99b4c4a 100644 --- a/backend/app.py +++ b/backend/app.py @@ -23,8 +23,8 @@ from backend.routes.dhcp import router as dhcp_router from backend.security import is_logged_in, apply_session # Import Settings & Logging -from settings.settings import settings -from log.log import get_logger +from backend.settings.settings import settings +from backend.log.log import get_logger # Logger initialization logger = get_logger(__name__) diff --git a/backend/bootstrap.py b/backend/bootstrap.py index ca3261e..4515acb 100644 --- a/backend/bootstrap.py +++ b/backend/bootstrap.py @@ -3,14 +3,17 @@ # Import standard modules import logging import os + # Import backend modules from backend.db.db import init_db +import backend.db.config import backend.db.users import backend.db.hosts import backend.db.aliases + # Import Settings & Logging -from settings.settings import settings -from log.log import setup_logging, get_logger +from backend.settings.settings import settings +from backend.log.log import setup_logging, get_logger # ------------------------------------------------------------------------------ # Welcome log diff --git a/backend/db/aliases.py b/backend/db/aliases.py index 038a770..cd2950c 100644 --- a/backend/db/aliases.py +++ b/backend/db/aliases.py @@ -6,11 +6,12 @@ import logging import os import re import sqlite3 + # Import local modules from backend.db.db import get_db, register_init -# Import Settings & Logging -from settings.settings import settings -from log.log import get_logger + +# Import Logging +from backend.log.log import setup_logging, get_logger # Logger initialization logger = get_logger(__name__) diff --git a/backend/db/config.py b/backend/db/config.py new file mode 100644 index 0000000..add67e2 --- /dev/null +++ b/backend/db/config.py @@ -0,0 +1,42 @@ +# backend/db/config.py + +# Import standard modules +import os +import sqlite3 + +# Import local modules +from backend.db.db import get_db, register_init + +# Import Settings & Logging +from backend.settings.settings import settings +from backend.log.log import setup_logging, get_logger + +# Logger initialization +logger = get_logger(__name__) + +# ----------------------------- +# Return a specific config value +# ----------------------------- +def get_config(key): + conn = get_db() + cur = conn.execute("SELECT value FROM config WHERE key = ?", (key,)) + row = cur.fetchone() + return row["value"] if row else None + +# ----------------------------- +# Initialize Config DB Table +# ----------------------------- +@register_init +def init_db_hosts_table(cur): + + # SETTINGS TABLE + cur.execute(""" + CREATE TABLE config ( + key TEXT PRIMARY KEY, + value TEXT + ); + """) + cur.execute("INSERT INTO config (key, value) VALUES (?, ?)", ("domain", settings.DOMAIN)) + cur.execute("INSERT INTO config (key, value) VALUES (?, ?)", ("external_name", settings.EXTERNAL_NAME)) + + logger.info("CONFIG DB: Tables initialized successfully") diff --git a/backend/db/db.py b/backend/db/db.py index 99257fe..8697f14 100644 --- a/backend/db/db.py +++ b/backend/db/db.py @@ -3,9 +3,10 @@ # Import standard modules import os import sqlite3 + # Import Settings & Logging -from settings.settings import settings -from log.log import get_logger +from backend.settings.settings import settings +from backend.log.log import setup_logging, get_logger # Logger initialization logger = get_logger(__name__) diff --git a/backend/db/hosts.py b/backend/db/hosts.py index 92c861c..b44d84a 100644 --- a/backend/db/hosts.py +++ b/backend/db/hosts.py @@ -6,11 +6,12 @@ import logging import os import re import sqlite3 + # Import local modules from backend.db.db import get_db, register_init -# Import Settings & Logging -from settings.settings import settings -from log.log import get_logger + +# Import Logging +from backend.log.log import setup_logging, get_logger # Logger initialization logger = get_logger(__name__) @@ -197,16 +198,6 @@ def delete_host(host_id: int) -> bool: @register_init def init_db_hosts_table(cur): - # SETTINGS TABLE - cur.execute(""" - CREATE TABLE settings ( - key TEXT PRIMARY KEY, - value TEXT - ); - """) - cur.execute("INSERT INTO settings (key, value) VALUES (?, ?)", ("domain", settings.DOMAIN)) - cur.execute("INSERT INTO settings (key, value) VALUES (?, ?)", ("external_name", settings.EXTERNAL_NAME)) - # HOSTS TABLE cur.execute(""" CREATE TABLE hosts ( diff --git a/backend/db/users.py b/backend/db/users.py index ffc6825..1a116b4 100644 --- a/backend/db/users.py +++ b/backend/db/users.py @@ -5,11 +5,13 @@ import bcrypt import json import logging import os + # Import local modules from backend.db.db import get_db, register_init + # Import Settings & Logging -from settings.settings import settings -from log.log import get_logger +from backend.settings.settings import settings +from backend.log.log import setup_logging, get_logger # Logger initialization logger = get_logger(__name__) diff --git a/backend/log/__init__.py b/backend/log/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/log/log.py b/backend/log/log.py new file mode 100644 index 0000000..18f43ab --- /dev/null +++ b/backend/log/log.py @@ -0,0 +1,164 @@ +# backend/log/log.py + +from __future__ import annotations + +import logging +import logging.config +import os +from typing import Optional + +# Module-level flag to prevent re-initialization +_INITIALIZED = False + +# --------------------------------------------------------- +# Build a full dictConfig for logging +# --------------------------------------------------------- +def build_log_config(level: str = "INFO", to_file: bool = False, log_file: Optional[str] = None, log_access_file: Optional[str] = None,) -> dict: + """ + Returns a complete dictConfig for the logging system, including formatters, + console/file handlers, and specific loggers (uvicorn, fastapi, etc). + """ + level = (level or "INFO").upper() + + formatters = { + # Generic Formatter + "detailed": { + "format": "%(asctime)s %(levelname)s [%(name)s] %(message)s", + "datefmt": "%Y-%m-%dT%H:%M:%S%z", + }, + # Access log Formatter + "access": { + "()": "uvicorn.logging.AccessFormatter", + "fmt": '%(asctime)s %(levelname)s [%(name)s] %(client_addr)s - "%(request_line)s" %(status_code)s', + "datefmt": "%Y-%m-%dT%H:%M:%S%z", + }, + } + + handlers = { + # Generic Console (root/uvicorn/uvicorn.error/fastapi/app) + "console": { + "class": "logging.StreamHandler", + "level": level, + "formatter": "detailed", + "stream": "ext://sys.stdout", + }, + # Access log Console + "access_console": { + "class": "logging.StreamHandler", + "level": level, + "formatter": "access", + "stream": "ext://sys.stdout", + }, + } + + # Select active handler based on console + active_handlers = ["console"] + access_handlers = ["access_console"] + + if to_file: + if log_file is not None: + # Ensure the log directory exists and add a rotating file handler + log_dir = os.path.dirname(log_file) or "." + os.makedirs(log_dir, exist_ok=True) + # handler for generic log + handlers["file"] = { + "class": "logging.handlers.RotatingFileHandler", + "level": level, + "formatter": "detailed", + "filename": log_file, + "maxBytes": 5 * 1024 * 1024, + "backupCount": 5, + "encoding": "utf-8", + } + # Add active handler for generic log file + active_handlers.append("file") + if log_access_file is not None: + # Ensure the log directory exists and add a rotating file handler + log_dir = os.path.dirname(log_access_file) or "." + os.makedirs(log_dir, exist_ok=True) + # handler for access log + handlers["access_file"] = { + "class": "logging.handlers.RotatingFileHandler", + "level": level, + "formatter": "access", + "filename": log_access_file, + "maxBytes": 5 * 1024 * 1024, + "backupCount": 5, + "encoding": "utf-8", + } + # Add active handler for access log file + access_handlers.append("access_file") + + return { + "version": 1, + "disable_existing_loggers": False, + "formatters": formatters, + "handlers": handlers, + + # Root logger + "root": { + "level": level, + "handlers": active_handlers, + }, + # Modules + "loggers": { + # Uvicorn core + "uvicorn": { + "level": level, + "handlers": active_handlers, + "propagate": False, + }, + # Uvicorn internal error + "uvicorn.error": { + "level": level, + "handlers": active_handlers, + "propagate": False, + }, + # Uvicorn access log + "uvicorn.access": { + "level": level, + "handlers": access_handlers, + "propagate": False, + }, + # FastAPI + "fastapi": { + "level": level, + "handlers": active_handlers, + "propagate": False, + }, + # Logger applicativo di comodo (puoi usarlo come logging.getLogger("main")) + "main": { + "level": level, + "handlers": active_handlers, + "propagate": False, + }, + }, + } + +# --------------------------------------------------------- +# Initialize logging once (singleton guard) +# --------------------------------------------------------- +def setup_logging(level: str = "INFO", to_file: bool = False, log_file: Optional[str] = None, log_access_file: Optional[str] = None, *, force: bool = False) -> None: + """ + Initializes the logging system only once. Subsequent calls are no-ops. + Useful to prevent duplicated handlers or reconfiguration side effects. + """ + global _INITIALIZED + + if _INITIALIZED and not force: + return + + config = build_log_config(level=level, to_file=to_file, log_file=log_file, log_access_file=log_access_file) + logging.config.dictConfig(config) + + _INITIALIZED = True + +# --------------------------------------------------------- +# Get a configured logger for the given module/name +# --------------------------------------------------------- +def get_logger(name: str | None = None) -> logging.Logger: + """ + Returns a logger instance configured via the module setup. If setup was not + called yet, it falls back to the standard logging defaults. + """ + return logging.getLogger(name or "main") diff --git a/backend/main.py b/backend/main.py index 67d407a..21c5818 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,6 +2,7 @@ # Import standard modules import os + # Import backend modules from backend.bootstrap import bootstrap from backend.app import create_app diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/routes/about.py b/backend/routes/about.py index 2cdf606..1f7a1ca 100644 --- a/backend/routes/about.py +++ b/backend/routes/about.py @@ -2,8 +2,12 @@ # Import standard modules from fastapi import APIRouter + +# Import local modules +from backend.db.config import get_config + # Import Settings -from settings.settings import settings +from backend.settings.settings import settings # Create Router router = APIRouter() @@ -13,10 +17,10 @@ router = APIRouter() # --------------------------------------------------------- @router.get("/about") def about(): + domain = get_config("domain") return { "app": { "version": settings.APP_VERSION, }, - "domain": settings.DOMAIN, - "admin_hash_loaded": settings.ADMIN_PASSWORD_HASH is not None, + "domain": domain, } diff --git a/backend/routes/aliases.py b/backend/routes/aliases.py index cd0f686..98bbd7d 100644 --- a/backend/routes/aliases.py +++ b/backend/routes/aliases.py @@ -6,6 +6,7 @@ from fastapi.responses import FileResponse, JSONResponse, RedirectResponse import ipaddress import time import os + # Import local modules from backend.db.aliases import ( get_aliases, @@ -14,9 +15,10 @@ from backend.db.aliases import ( update_alias, delete_alias ) + # Import Settings & Logging -from settings.settings import settings -from log.log import get_logger +from backend.settings.settings import settings +from backend.log.log import setup_logging, get_logger # Logger initialization logger = get_logger(__name__) diff --git a/backend/routes/backup.py b/backend/routes/backup.py index 31d86e9..ae70764 100644 --- a/backend/routes/backup.py +++ b/backend/routes/backup.py @@ -8,11 +8,13 @@ import json import os import ipaddress import time + # Import local modules from backend.db.hosts import get_hosts + # Import Settings & Logging -from settings.settings import settings -from log.log import get_logger +from backend.settings.settings import settings +from backend.log.log import setup_logging, get_logger # Logger initialization logger = get_logger(__name__) diff --git a/backend/routes/dhcp.py b/backend/routes/dhcp.py index 73fe0e8..72755a8 100644 --- a/backend/routes/dhcp.py +++ b/backend/routes/dhcp.py @@ -10,11 +10,13 @@ import os import ipaddress from pathlib import Path import time + # Import local modules from backend.db.hosts import get_hosts + # Import Settings & Logging -from settings.settings import settings -from log.log import get_logger +from backend.settings.settings import settings +from backend.log.log import setup_logging, get_logger # Logger initialization logger = get_logger(__name__) diff --git a/backend/routes/dns.py b/backend/routes/dns.py index c260865..3b06526 100644 --- a/backend/routes/dns.py +++ b/backend/routes/dns.py @@ -8,12 +8,15 @@ import json import os import ipaddress import time + # Import local modules +from backend.db.config import get_config from backend.db.hosts import get_hosts from backend.db.aliases import get_aliases + # Import Settings & Logging -from settings.settings import settings -from log.log import get_logger +from backend.settings.settings import settings +from backend.log.log import setup_logging, get_logger # Logger initialization logger = get_logger(__name__) @@ -41,6 +44,9 @@ async def api_dns_reload(request: Request): line = f"{h.get('name')}\t\t IN\tA\t{h.get('ipv4')}\n" f.write(line) + # Get Domain + domain = get_config("domain") + # Save DNS Reverse Configuration path = settings.DNS_REVERSE_FILE with open(path, "w", encoding="utf-8") as f: @@ -49,11 +55,11 @@ async def api_dns_reload(request: Request): 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" + line = f"{rev}\t\t IN PTR\t{h.get('name')}.{domain}\n" f.write(line) # Get Aliases List - hosts = get_aliases() + aliases = get_aliases() # Save DNS Aliases Configuration path = settings.DNS_ALIAS_FILE diff --git a/backend/routes/health.py b/backend/routes/health.py index bf9cc3f..c357520 100644 --- a/backend/routes/health.py +++ b/backend/routes/health.py @@ -5,9 +5,10 @@ from fastapi import APIRouter import sqlite3 import time import os + # Import Settings & Logging -from settings.settings import settings -from log.log import get_logger +from backend.settings.settings import settings +from backend.log.log import setup_logging, get_logger # Logger initialization logger = get_logger(__name__) diff --git a/backend/routes/hosts.py b/backend/routes/hosts.py index bcb26d0..4864830 100644 --- a/backend/routes/hosts.py +++ b/backend/routes/hosts.py @@ -6,6 +6,7 @@ from fastapi.responses import FileResponse, JSONResponse, RedirectResponse import ipaddress import time import os + # Import local modules from backend.db.hosts import ( get_hosts, @@ -14,9 +15,10 @@ from backend.db.hosts import ( update_host, delete_host ) + # Import Settings & Logging -from settings.settings import settings -from log.log import get_logger +from backend.settings.settings import settings +from backend.log.log import setup_logging, get_logger # Logger initialization logger = get_logger(__name__) diff --git a/backend/routes/login.py b/backend/routes/login.py index 42ff197..608ba85 100644 --- a/backend/routes/login.py +++ b/backend/routes/login.py @@ -5,10 +5,12 @@ from fastapi import APIRouter, Request, Response, HTTPException, status from fastapi.responses import FileResponse, RedirectResponse import os import time + # Import local modules from backend.security import verify_login, apply_session, close_session -# Import Settings -from settings.settings import settings + +# Import Settings & Logging +from backend.settings.settings import settings # Create Router router = APIRouter() diff --git a/backend/security.py b/backend/security.py index 69b49ce..99f8db6 100644 --- a/backend/security.py +++ b/backend/security.py @@ -5,11 +5,13 @@ import bcrypt import os from fastapi import Request, HTTPException from itsdangerous import TimestampSigner + # Import local modules from backend.db.users import get_user_by_username + # Import Settings & Logging -from settings.settings import settings -from log.log import get_logger +from backend.settings.settings import settings +from backend.log.log import setup_logging, get_logger # Logger initialization logger = get_logger(__name__) diff --git a/backend/server.py b/backend/server.py index 3b73281..d69eaf3 100644 --- a/backend/server.py +++ b/backend/server.py @@ -4,8 +4,8 @@ import uvicorn # Import Settings & Logging -from settings.settings import settings -from log.log import get_logger +from backend.settings.settings import settings +from backend.log.log import setup_logging, get_logger # Logger initialization logger = get_logger(__name__) diff --git a/backend/settings/__init__.py b/backend/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/settings/config.py b/backend/settings/config.py new file mode 100644 index 0000000..e5aea1a --- /dev/null +++ b/backend/settings/config.py @@ -0,0 +1,7 @@ +# backend/settings/config.py + +# --------------------------------------------------------- +# APP +# --------------------------------------------------------- +APP_NAME = "network-manager" +APP_VERSION = "0.0.2" diff --git a/backend/settings/default.py b/backend/settings/default.py new file mode 100644 index 0000000..906e1a2 --- /dev/null +++ b/backend/settings/default.py @@ -0,0 +1,61 @@ +# backend/settings/default.py + +# --------------------------------------------------------- +# Frontend +# --------------------------------------------------------- +FRONTEND_DIR = "/app/frontend" + +# --------------------------------------------------------- +# Data Path (DB + Backup) +# --------------------------------------------------------- +DATA_PATH = "/data" + +# --------------------------------------------------------- +# Database +# --------------------------------------------------------- +DB_FILE = "database.db" +DB_RESET = False + +# --------------------------------------------------------- +# Log +# --------------------------------------------------------- +LOG_LEVEL = "INFO" +LOG_TO_FILE = False +LOG_FILE = "app.log" +LOG_ACCESS_FILE = "access.log" + +# --------------------------------------------------------- +# Host +# --------------------------------------------------------- +DOMAIN = "example.com" +EXTERNAL_NAME = "dyndns.example.com" + +# --------------------------------------------------------- +# Web +# --------------------------------------------------------- +HTTP_HOST = "0.0.0.0" +HTTP_PORT = "8000" +LOGIN_MAX_ATTEMPTS = "5" +LOGIN_WINDOW_SECONDS = "600" + +# --------------------------------------------------------- +# Admin +# --------------------------------------------------------- +ADMIN_USER = "admin" +ADMIN_PASSWORD = "admin" +ADMIN_PASSWORD_HASH_FILE = "/run/secrets/admin_password_hash" + +# --------------------------------------------------------- +# DNS +# --------------------------------------------------------- +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 +# --------------------------------------------------------- +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" diff --git a/backend/settings/settings.py b/backend/settings/settings.py new file mode 100644 index 0000000..aacb5da --- /dev/null +++ b/backend/settings/settings.py @@ -0,0 +1,118 @@ +# backend/settings/settings.py + +from __future__ import annotations + +# import standard modules +import os +import secrets +import datetime +from pathlib import Path +from typing import Optional +from pydantic import BaseModel, Field, field_validator + +# Import Parameters +from . import config, default + +# --------------------------------------------------------- +# Convert value to boolean +# --------------------------------------------------------- +def _to_bool(val) -> bool: + if isinstance(val, bool): + return val + if val is None: + return False + return str(val).strip().lower() in {"1", "true", "yes", "on"} + +# --------------------------------------------------------- +# Read text from file if it exists +# --------------------------------------------------------- +def _read_text_if_exists(path: Optional[str]) -> Optional[str]: + if not path: + return None + p = Path(path) + if p.exists() and p.is_file(): + try: + return p.read_text(encoding="utf-8").strip() + except Exception: + return None + return None + +# --------------------------------------------------------- +# Settings Model +# --------------------------------------------------------- +class Settings(BaseModel): + # Naming + APP_NAME: str = Field(default_factory=lambda: config.APP_NAME) + + # Versioning + APP_VERSION: str = Field(default_factory=lambda: config.APP_VERSION) + DEVEL: bool = Field(default_factory=lambda: _to_bool(os.getenv("DEV", False))) + + # 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)) + + # Database + DB_FILE: str = Field(default_factory=lambda: os.getenv("DB_FILE", default.DB_FILE)) + DB_RESET: bool = Field(default_factory=lambda: _to_bool(os.getenv("DB_RESET", default.DB_RESET))) + + # Log + LOG_LEVEL: str = Field(default_factory=lambda: os.getenv("LOG_LEVEL", default.LOG_LEVEL)) + LOG_TO_FILE: bool = Field(default_factory=lambda: _to_bool(os.getenv("LOG_TO_FILE", default.LOG_TO_FILE))) + LOG_FILE: str = Field(default_factory=lambda: os.getenv("LOG_FILE", default.LOG_FILE)) + LOG_ACCESS_FILE: str = Field(default_factory=lambda: os.getenv("LOG_ACCESS_FILE", default.LOG_ACCESS_FILE)) + + # Hosts + DOMAIN: str = Field(default_factory=lambda: os.getenv("DOMAIN", default.DOMAIN)) + EXTERNAL_NAME: str = Field(default_factory=lambda: os.getenv("EXTERNAL_NAME", default.DOMAIN)) + + # Web + HTTP_HOST: str = Field(default_factory=lambda: os.getenv("HTTP_HOST", default.HTTP_HOST)) + HTTP_PORT: int = Field(default_factory=lambda: int(os.getenv("HTTP_PORT", default.HTTP_PORT))) + SECRET_KEY: str = Field(default_factory=lambda: ( + (os.getenv("SESSION_SECRET") or _read_text_if_exists(os.getenv("SECRET_KEY_FILE")) or secrets.token_urlsafe(64)).strip() + )) + LOGIN_MAX_ATTEMPTS: int = Field(default_factory=lambda: int(os.getenv("LOGIN_MAX_ATTEMPTS", default.LOGIN_MAX_ATTEMPTS))) + LOGIN_WINDOW_SECONDS: int = Field(default_factory=lambda: int(os.getenv("LOGIN_WINDOW_SECONDS", default.LOGIN_WINDOW_SECONDS))) + + # Admin + ADMIN_USER: str = Field(default_factory=lambda: os.getenv("ADMIN_USER", default.ADMIN_USER)) + ADMIN_PASSWORD: str = Field(default_factory=lambda: os.getenv("ADMIN_PASSWORD", default.ADMIN_PASSWORD)) + ADMIN_PASSWORD_HASH_FILE: str = Field(default_factory=lambda: os.getenv("ADMIN_PASSWORD_HASH_FILE", default.ADMIN_PASSWORD_HASH_FILE)) + ADMIN_PASSWORD_HASH: Optional[str] = Field(default_factory=lambda: ( + (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_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 + 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)) + DHCP6_LEASES_FILE: str = Field(default_factory=lambda: os.getenv("DHCP6_LEASES_FILE", default.DHCP6_LEASES_FILE)) + + def model_post_init(self, __context) -> None: + if self.DEVEL: + ts = datetime.datetime.now().strftime("%Y%m%d-%H%M") + object.__setattr__(self, "APP_VERSION", f"{self.APP_VERSION}-dev-{ts}") + else: + object.__setattr__(self, "APP_VERSION", self.APP_VERSION) + + # 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) + +# --------------------------------------------------------- +# Singleton +# --------------------------------------------------------- +settings = Settings() diff --git a/log/__init__.py b/log/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/log/log.py b/log/log.py deleted file mode 100644 index 899b179..0000000 --- a/log/log.py +++ /dev/null @@ -1,164 +0,0 @@ -# log/log.py - -from __future__ import annotations - -import logging -import logging.config -import os -from typing import Optional - -# Module-level flag to prevent re-initialization -_INITIALIZED = False - -# --------------------------------------------------------- -# Build a full dictConfig for logging -# --------------------------------------------------------- -def build_log_config(level: str = "INFO", to_file: bool = False, log_file: Optional[str] = None, log_access_file: Optional[str] = None,) -> dict: - """ - Returns a complete dictConfig for the logging system, including formatters, - console/file handlers, and specific loggers (uvicorn, fastapi, etc). - """ - level = (level or "INFO").upper() - - formatters = { - # Generic Formatter - "detailed": { - "format": "%(asctime)s %(levelname)s [%(name)s] %(message)s", - "datefmt": "%Y-%m-%dT%H:%M:%S%z", - }, - # Access log Formatter - "access": { - "()": "uvicorn.logging.AccessFormatter", - "fmt": '%(asctime)s %(levelname)s [%(name)s] %(client_addr)s - "%(request_line)s" %(status_code)s', - "datefmt": "%Y-%m-%dT%H:%M:%S%z", - }, - } - - handlers = { - # Generic Console (root/uvicorn/uvicorn.error/fastapi/app) - "console": { - "class": "logging.StreamHandler", - "level": level, - "formatter": "detailed", - "stream": "ext://sys.stdout", - }, - # Access log Console - "access_console": { - "class": "logging.StreamHandler", - "level": level, - "formatter": "access", - "stream": "ext://sys.stdout", - }, - } - - # Select active handler based on console - active_handlers = ["console"] - access_handlers = ["access_console"] - - if to_file: - if log_file is not None: - # Ensure the log directory exists and add a rotating file handler - log_dir = os.path.dirname(log_file) or "." - os.makedirs(log_dir, exist_ok=True) - # handler for generic log - handlers["file"] = { - "class": "logging.handlers.RotatingFileHandler", - "level": level, - "formatter": "detailed", - "filename": log_file, - "maxBytes": 5 * 1024 * 1024, - "backupCount": 5, - "encoding": "utf-8", - } - # Add active handler for generic log file - active_handlers.append("file") - if log_access_file is not None: - # Ensure the log directory exists and add a rotating file handler - log_dir = os.path.dirname(log_access_file) or "." - os.makedirs(log_dir, exist_ok=True) - # handler for access log - handlers["access_file"] = { - "class": "logging.handlers.RotatingFileHandler", - "level": level, - "formatter": "access", - "filename": log_access_file, - "maxBytes": 5 * 1024 * 1024, - "backupCount": 5, - "encoding": "utf-8", - } - # Add active handler for access log file - access_handlers.append("access_file") - - return { - "version": 1, - "disable_existing_loggers": False, - "formatters": formatters, - "handlers": handlers, - - # Root logger - "root": { - "level": level, - "handlers": active_handlers, - }, - # Modules - "loggers": { - # Uvicorn core - "uvicorn": { - "level": level, - "handlers": active_handlers, - "propagate": False, - }, - # Uvicorn internal error - "uvicorn.error": { - "level": level, - "handlers": active_handlers, - "propagate": False, - }, - # Uvicorn access log - "uvicorn.access": { - "level": level, - "handlers": access_handlers, - "propagate": False, - }, - # FastAPI - "fastapi": { - "level": level, - "handlers": active_handlers, - "propagate": False, - }, - # Logger applicativo di comodo (puoi usarlo come logging.getLogger("main")) - "main": { - "level": level, - "handlers": active_handlers, - "propagate": False, - }, - }, - } - -# --------------------------------------------------------- -# Initialize logging once (singleton guard) -# --------------------------------------------------------- -def setup_logging(level: str = "INFO", to_file: bool = False, log_file: Optional[str] = None, log_access_file: Optional[str] = None, *, force: bool = False) -> None: - """ - Initializes the logging system only once. Subsequent calls are no-ops. - Useful to prevent duplicated handlers or reconfiguration side effects. - """ - global _INITIALIZED - - if _INITIALIZED and not force: - return - - config = build_log_config(level=level, to_file=to_file, log_file=log_file, log_access_file=log_access_file) - logging.config.dictConfig(config) - - _INITIALIZED = True - -# --------------------------------------------------------- -# Get a configured logger for the given module/name -# --------------------------------------------------------- -def get_logger(name: str | None = None) -> logging.Logger: - """ - Returns a logger instance configured via the module setup. If setup was not - called yet, it falls back to the standard logging defaults. - """ - return logging.getLogger(name or "main") diff --git a/settings/__init__.py b/settings/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/settings/config.py b/settings/config.py deleted file mode 100644 index 052f689..0000000 --- a/settings/config.py +++ /dev/null @@ -1,7 +0,0 @@ -# backend/config.py - -# --------------------------------------------------------- -# APP -# --------------------------------------------------------- -APP_NAME = "network-manager" -APP_VERSION = "0.0.2" diff --git a/settings/default.py b/settings/default.py deleted file mode 100644 index 2caf0e9..0000000 --- a/settings/default.py +++ /dev/null @@ -1,61 +0,0 @@ -# backend/default.py - -# --------------------------------------------------------- -# Frontend -# --------------------------------------------------------- -FRONTEND_DIR = "/app/frontend" - -# --------------------------------------------------------- -# Data Path (DB + Backup) -# --------------------------------------------------------- -DATA_PATH = "/data" - -# --------------------------------------------------------- -# Database -# --------------------------------------------------------- -DB_FILE = "database.db" -DB_RESET = False - -# --------------------------------------------------------- -# Log -# --------------------------------------------------------- -LOG_LEVEL = "INFO" -LOG_TO_FILE = False -LOG_FILE = "app.log" -LOG_ACCESS_FILE = "access.log" - -# --------------------------------------------------------- -# Host -# --------------------------------------------------------- -DOMAIN = "example.com" -EXTERNAL_NAME = "dyndns.example.com" - -# --------------------------------------------------------- -# Web -# --------------------------------------------------------- -HTTP_HOST = "0.0.0.0" -HTTP_PORT = "8000" -LOGIN_MAX_ATTEMPTS = "5" -LOGIN_WINDOW_SECONDS = "600" - -# --------------------------------------------------------- -# Admin -# --------------------------------------------------------- -ADMIN_USER = "admin" -ADMIN_PASSWORD = "admin" -ADMIN_PASSWORD_HASH_FILE = "/run/secrets/admin_password_hash" - -# --------------------------------------------------------- -# DNS -# --------------------------------------------------------- -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 -# --------------------------------------------------------- -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" diff --git a/settings/settings.py b/settings/settings.py deleted file mode 100644 index 69e5e76..0000000 --- a/settings/settings.py +++ /dev/null @@ -1,117 +0,0 @@ -# backend/settings.py - -from __future__ import annotations - -# import standard modules -import os -import secrets -import datetime -from pathlib import Path -from typing import Optional -from pydantic import BaseModel, Field, field_validator -# Import Parameters -from . import config, default - -# --------------------------------------------------------- -# Convert value to boolean -# --------------------------------------------------------- -def _to_bool(val) -> bool: - if isinstance(val, bool): - return val - if val is None: - return False - return str(val).strip().lower() in {"1", "true", "yes", "on"} - -# --------------------------------------------------------- -# Read text from file if it exists -# --------------------------------------------------------- -def _read_text_if_exists(path: Optional[str]) -> Optional[str]: - if not path: - return None - p = Path(path) - if p.exists() and p.is_file(): - try: - return p.read_text(encoding="utf-8").strip() - except Exception: - return None - return None - -# --------------------------------------------------------- -# Settings Model -# --------------------------------------------------------- -class Settings(BaseModel): - # Naming - APP_NAME: str = Field(default_factory=lambda: config.APP_NAME) - - # Versioning - APP_VERSION: str = Field(default_factory=lambda: config.APP_VERSION) - DEVEL: bool = Field(default_factory=lambda: _to_bool(os.getenv("DEV", False))) - - # 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)) - - # Database - DB_FILE: str = Field(default_factory=lambda: os.getenv("DB_FILE", default.DB_FILE)) - DB_RESET: bool = Field(default_factory=lambda: _to_bool(os.getenv("DB_RESET", default.DB_RESET))) - - # Log - LOG_LEVEL: str = Field(default_factory=lambda: os.getenv("LOG_LEVEL", default.LOG_LEVEL)) - LOG_TO_FILE: bool = Field(default_factory=lambda: _to_bool(os.getenv("LOG_TO_FILE", default.LOG_TO_FILE))) - LOG_FILE: str = Field(default_factory=lambda: os.getenv("LOG_FILE", default.LOG_FILE)) - LOG_ACCESS_FILE: str = Field(default_factory=lambda: os.getenv("LOG_ACCESS_FILE", default.LOG_ACCESS_FILE)) - - # Hosts - DOMAIN: str = Field(default_factory=lambda: os.getenv("DOMAIN", default.DOMAIN)) - EXTERNAL_NAME: str = Field(default_factory=lambda: os.getenv("EXTERNAL_NAME", default.DOMAIN)) - - # Web - HTTP_HOST: str = Field(default_factory=lambda: os.getenv("HTTP_HOST", default.HTTP_HOST)) - HTTP_PORT: int = Field(default_factory=lambda: int(os.getenv("HTTP_PORT", default.HTTP_PORT))) - SECRET_KEY: str = Field(default_factory=lambda: ( - (os.getenv("SESSION_SECRET") or _read_text_if_exists(os.getenv("SECRET_KEY_FILE")) or secrets.token_urlsafe(64)).strip() - )) - LOGIN_MAX_ATTEMPTS: int = Field(default_factory=lambda: int(os.getenv("LOGIN_MAX_ATTEMPTS", default.LOGIN_MAX_ATTEMPTS))) - LOGIN_WINDOW_SECONDS: int = Field(default_factory=lambda: int(os.getenv("LOGIN_WINDOW_SECONDS", default.LOGIN_WINDOW_SECONDS))) - - # Admin - ADMIN_USER: str = Field(default_factory=lambda: os.getenv("ADMIN_USER", default.ADMIN_USER)) - ADMIN_PASSWORD: str = Field(default_factory=lambda: os.getenv("ADMIN_PASSWORD", default.ADMIN_PASSWORD)) - ADMIN_PASSWORD_HASH_FILE: str = Field(default_factory=lambda: os.getenv("ADMIN_PASSWORD_HASH_FILE", default.ADMIN_PASSWORD_HASH_FILE)) - ADMIN_PASSWORD_HASH: Optional[str] = Field(default_factory=lambda: ( - (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_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 - 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)) - DHCP6_LEASES_FILE: str = Field(default_factory=lambda: os.getenv("DHCP6_LEASES_FILE", default.DHCP6_LEASES_FILE)) - - def model_post_init(self, __context) -> None: - if self.DEVEL: - ts = datetime.datetime.now().strftime("%Y%m%d-%H%M") - object.__setattr__(self, "APP_VERSION", f"{self.APP_VERSION}-dev-{ts}") - else: - object.__setattr__(self, "APP_VERSION", self.APP_VERSION) - - # 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) - -# --------------------------------------------------------- -# Singleton -# --------------------------------------------------------- -settings = Settings()