2026-05-18 15:13:31 +00:00
|
|
|
#!/usr/bin/env python3
|
2026-05-18 21:06:20 +00:00
|
|
|
"""Shared utilities for ns8-backup-monitor.
|
|
|
|
|
|
|
|
|
|
This module provides two thin helpers used by every other module in the package:
|
|
|
|
|
|
|
|
|
|
load_config()
|
|
|
|
|
Locate and parse the YAML configuration file. The caller may supply an
|
|
|
|
|
explicit path; when omitted, a built-in list of well-known locations is
|
|
|
|
|
probed in order.
|
|
|
|
|
|
|
|
|
|
setup_logging()
|
|
|
|
|
Initialise the root Python logger from the ``logging`` section of the
|
|
|
|
|
parsed configuration. A StreamHandler (stdout) is always added so that
|
|
|
|
|
journald captures output when the service runs under systemd. An optional
|
|
|
|
|
rotating file handler is added when ``logging.file`` is set.
|
|
|
|
|
|
|
|
|
|
Both functions are called once during start-up from ``__main__.py`` before any
|
|
|
|
|
other module-level loggers are used.
|
2026-05-18 15:13:31 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
import logging.handlers
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
import yaml
|
|
|
|
|
except ImportError:
|
2026-05-18 21:06:20 +00:00
|
|
|
# PyYAML is the only non-stdlib dependency. We defer the error to
|
|
|
|
|
# load_config() so that a helpful message is shown at runtime instead of
|
|
|
|
|
# a bare ImportError traceback.
|
2026-05-18 15:13:31 +00:00
|
|
|
yaml = None
|
|
|
|
|
|
|
|
|
|
|
2026-05-18 21:06:20 +00:00
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Default config search paths
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Tried in order when no explicit --config argument is given.
|
|
|
|
|
# The last entry (config/config.yml) allows running directly from the repo
|
|
|
|
|
# root during development without a system-wide installation.
|
2026-05-18 15:13:31 +00:00
|
|
|
DEFAULT_CONFIG_PATHS = [
|
2026-05-18 21:06:20 +00:00
|
|
|
"/etc/ns8-backup-monitor/config.yml", # production install path
|
|
|
|
|
"/opt/ns8-backup-monitor/config/config.yml", # alternative install layout
|
|
|
|
|
"config/config.yml", # development / repo root
|
2026-05-18 15:13:31 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_config(path: str = None) -> dict:
|
2026-05-18 21:06:20 +00:00
|
|
|
"""Locate and parse the YAML configuration file.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
path: Explicit filesystem path to ``config.yml``. When ``None``,
|
|
|
|
|
``DEFAULT_CONFIG_PATHS`` is probed in order and the first
|
|
|
|
|
existing file is used.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Parsed configuration as a plain Python dict. Returns an empty dict
|
|
|
|
|
if the file exists but is empty.
|
|
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
|
ImportError: PyYAML is not installed.
|
|
|
|
|
FileNotFoundError: No config file found at any of the probed paths.
|
|
|
|
|
"""
|
2026-05-18 15:13:31 +00:00
|
|
|
if yaml is None:
|
|
|
|
|
raise ImportError("PyYAML not installed. Run: pip3 install pyyaml")
|
|
|
|
|
|
2026-05-18 21:06:20 +00:00
|
|
|
# Use the caller-supplied path or fall back to the default search list.
|
2026-05-18 15:13:31 +00:00
|
|
|
paths = [path] if path else DEFAULT_CONFIG_PATHS
|
2026-05-18 21:06:20 +00:00
|
|
|
|
2026-05-18 15:13:31 +00:00
|
|
|
for p in paths:
|
|
|
|
|
if p and Path(p).exists():
|
|
|
|
|
with open(p) as f:
|
|
|
|
|
return yaml.safe_load(f) or {}
|
|
|
|
|
|
|
|
|
|
raise FileNotFoundError(
|
|
|
|
|
f"No config file found. Tried: {paths}\n"
|
|
|
|
|
"Copy config/config.yml.example to config/config.yml and edit it."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def setup_logging(config: dict):
|
2026-05-18 21:06:20 +00:00
|
|
|
"""Initialise the root logger from the ``logging`` section of *config*.
|
|
|
|
|
|
|
|
|
|
Called once from ``__main__.py`` before any module-level loggers emit
|
|
|
|
|
records. Subsequent calls are no-ops because ``basicConfig`` is
|
|
|
|
|
idempotent once handlers are attached.
|
|
|
|
|
|
|
|
|
|
Configuration keys (all optional):
|
|
|
|
|
logging.level : Python log level name, e.g. ``INFO``, ``DEBUG``.
|
|
|
|
|
Defaults to ``INFO``.
|
|
|
|
|
logging.file : Absolute path for the rotating log file.
|
|
|
|
|
When empty or absent, only stdout is used.
|
|
|
|
|
|
|
|
|
|
The rotating file handler caps each file at 5 MB and keeps 3 backups,
|
|
|
|
|
which limits total disk usage to ~20 MB regardless of uptime.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
config: Parsed configuration dictionary (output of ``load_config``).
|
|
|
|
|
"""
|
2026-05-18 15:13:31 +00:00
|
|
|
log_cfg = config.get("logging", {})
|
2026-05-18 21:06:20 +00:00
|
|
|
|
|
|
|
|
# Resolve the log level; fall back to INFO for unknown names.
|
2026-05-18 15:13:31 +00:00
|
|
|
level = getattr(logging, log_cfg.get("level", "INFO").upper(), logging.INFO)
|
|
|
|
|
log_file = log_cfg.get("file", "")
|
|
|
|
|
|
2026-05-18 21:06:20 +00:00
|
|
|
# Always log to stdout so journald captures output via
|
|
|
|
|
# StandardOutput=journal in the systemd unit.
|
2026-05-18 15:13:31 +00:00
|
|
|
handlers = [logging.StreamHandler(sys.stdout)]
|
2026-05-18 21:06:20 +00:00
|
|
|
|
2026-05-18 15:13:31 +00:00
|
|
|
if log_file:
|
2026-05-18 21:06:20 +00:00
|
|
|
# Rotating file handler: 5 MB per file, 3 rotated backups kept.
|
2026-05-18 15:13:31 +00:00
|
|
|
handlers.append(
|
|
|
|
|
logging.handlers.RotatingFileHandler(
|
|
|
|
|
log_file, maxBytes=5 * 1024 * 1024, backupCount=3
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
logging.basicConfig(
|
|
|
|
|
level=level,
|
|
|
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
|
|
|
handlers=handlers,
|
|
|
|
|
)
|