This commit is contained in:
Jonas Bengtsson
2026-09-16 06:08:37 +02:00
parent cce6a4960e
commit 8d2c54beae
54 changed files with 8480 additions and 41 deletions
+121
View File
@@ -0,0 +1,121 @@
"""mosquitto.conf generator — PostgreSQL model to config text (spec sections 61, 25).
The generated configuration MUST avoid the options deprecated in
Mosquitto 2.1 (password_file, acl_file, per_listener_settings): those are
removed in 3.0 (spec section 3.1 note).
"""
from __future__ import annotations
from dataclasses import dataclass, field
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.models import BrokerSettings, ListenerConfig
_BOOL = {True: "true", False: "false"}
@dataclass
class GenerationResult:
config_text: str
warnings: list[str] = field(default_factory=list)
def _global_section(db: Session, warnings: list[str]) -> list[str]:
lines: list[str] = []
for setting in db.scalars(select(BrokerSettings)).all():
key = setting.key.strip()
value = setting.value.strip()
if not key or key.startswith("#"):
continue
lines.append(f"{key} {value}")
return lines
def _listener_block(listener: ListenerConfig, warnings: list[str]) -> list[str]:
lines: list[str] = []
if not listener.enabled:
return lines
lines.append(f"listener {listener.port} {listener.bind_address or ''}".rstrip())
if listener.bind_address is None:
# `listener <port>` without bind address binds all interfaces.
lines[-1] = f"listener {listener.port}"
lines.append(f"protocol {listener.protocol}")
if listener.allow_anonymous:
lines.append("listener_allow_anonymous true")
if listener.max_connections != -1:
lines.append(f"max_connections {listener.max_connections}")
if listener.tls_enabled:
# TLS blocks only become valid once PKI ships (Release 2); we
# generate them now so the model is forward-compatible, but warn.
warnings.append(
f"listener '{listener.name}': TLS requested but PKI is not "
"available in Release 1; certfile/keyfile not set"
)
if listener.tls_min_version:
lines.append(f"tls_version {listener.tls_min_version}")
if listener.require_certificate:
lines.append("require_certificate true")
if listener.use_identity_as_username:
lines.append("use_identity_as_username true")
return lines
def generate_config(db: Session) -> GenerationResult:
"""Build the complete mosquitto.conf from the database model.
The platform ALWAYS regenerates the full file; it never edits the
active file in place (spec section 61).
"""
warnings: list[str] = []
listeners = db.scalars(select(ListenerConfig)).all()
enabled = [item for item in listeners if item.enabled]
lines: list[str] = []
lines.append("# Generated by MQTT Management Platform — DO NOT EDIT BY HAND")
lines.append("# Ownership: PostgreSQL model -> revision -> this file (spec 61)")
lines.append("")
lines.append("# --- Global settings ---")
lines.append("per_listener_settings false")
lines.append("allow_anonymous false")
global_lines = _global_section(db, warnings)
if global_lines:
lines.extend(global_lines)
lines.append("")
# Dynamic security plugin: global, so it applies to every listener.
lines.append("# --- Dynamic Security ---")
lines.append("global_plugin /usr/lib/mosquitto_dynamic_security.so")
lines.append("plugin_opt_config_file /mosquitto/config/dynamic-security.json")
lines.append("")
if enabled:
lines.append("# --- Listeners ---")
for listener in enabled:
block = _listener_block(listener, warnings)
if block:
lines.append(f"# listener: {listener.name}")
lines.extend(block)
lines.append("")
else:
warnings.append(
"no enabled listeners; broker would fall back to loopback-only "
"with anonymous access — refusing to generate"
)
raise ValueError("no enabled listeners in configuration model")
lines.append("# --- Persistence ---")
lines.append("persistence true")
lines.append("persistence_location /mosquitto/data/")
lines.append("")
lines.append("# --- Logging ---")
lines.append("log_dest stdout")
lines.append("log_timestamp true")
lines.append("log_timestamp_format %Y-%m-%dT%H:%M:%S")
return GenerationResult(config_text="\n".join(lines) + "\n", warnings=warnings)