Files
NODEDC_MISSION_CORE/deploy/telemetry-plane/prepare.py
T

197 lines
6.2 KiB
Python

from __future__ import annotations
import argparse
import ipaddress
import os
import re
import secrets
import subprocess
from pathlib import Path
from typing import Final
ROOT: Final = Path(__file__).resolve().parent
ENV_PATH: Final = ROOT / ".env"
RUNTIME: Final = ROOT / "runtime" / "mosquitto"
IMAGE: Final = "eclipse-mosquitto:2.1.2-alpine"
PLACEHOLDER: Final = "replace-with-"
SAFE_IDENTIFIER: Final = re.compile(
r"^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$"
)
def _initialize_environment(bind_address: str, *, overwrite: bool = False) -> None:
if ENV_PATH.exists() and not overwrite:
raise RuntimeError(".env already exists; refusing to overwrite local credentials")
normalized_bind_address = str(ipaddress.ip_address(bind_address))
values = {
"MISSIONCORE_MQTT_BIND_ADDRESS": normalized_bind_address,
"MISSIONCORE_MQTT_PORT": "1883",
"MISSIONCORE_TELEMETRY_QUERY_PORT": "18030",
"MISSIONCORE_DB_NAME": "missioncore_telemetry",
"MISSIONCORE_DB_USER": "missioncore_ingest",
"MISSIONCORE_DB_PASSWORD": secrets.token_urlsafe(36),
"MISSIONCORE_DB_INGEST_PASSWORD": secrets.token_urlsafe(36),
"MISSIONCORE_MQTT_INGEST_USER": "missioncore-ingest",
"MISSIONCORE_MQTT_INGEST_PASSWORD": secrets.token_urlsafe(36),
"MISSIONCORE_MQTT_WORKER_006_USER": "worker-006",
"MISSIONCORE_MQTT_WORKER_006_CONTOUR": "worker-006",
"MISSIONCORE_MQTT_WORKER_006_PASSWORD": secrets.token_urlsafe(36),
}
ENV_PATH.write_text(
"".join(f"{name}={value}\n" for name, value in values.items()),
encoding="utf-8",
)
os.chmod(ENV_PATH, 0o600)
def _environment() -> dict[str, str]:
if not ENV_PATH.is_file():
raise RuntimeError("copy .env.example to .env and set unique secrets first")
values: dict[str, str] = {}
for raw_line in ENV_PATH.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
name, value = line.split("=", 1)
values[name.strip()] = value.strip()
return values
def _migrate_environment() -> None:
values = _environment()
additions: dict[str, str] = {}
if "MISSIONCORE_DB_INGEST_PASSWORD" not in values:
additions["MISSIONCORE_DB_INGEST_PASSWORD"] = secrets.token_urlsafe(36)
if "MISSIONCORE_MQTT_WORKER_006_CONTOUR" not in values:
additions["MISSIONCORE_MQTT_WORKER_006_CONTOUR"] = "worker-006"
if not additions:
return
with ENV_PATH.open("a", encoding="utf-8", newline="\n") as stream:
for name, value in additions.items():
stream.write(f"{name}={value}\n")
stream.flush()
os.fsync(stream.fileno())
os.chmod(ENV_PATH, 0o600)
def _required(values: dict[str, str], name: str) -> str:
value = values.get(name, "")
if not value or value.startswith(PLACEHOLDER):
raise RuntimeError(f"{name} must contain a non-placeholder value")
return value
def _identifier(values: dict[str, str], name: str) -> str:
value = _required(values, name)
if SAFE_IDENTIFIER.fullmatch(value) is None:
raise RuntimeError(f"{name} must contain a DNS-safe lowercase identifier")
return value
def _password_entry(path: Path, username: str, password: str, *, create: bool) -> None:
command = [
"docker",
"run",
"--rm",
"-i",
"-v",
f"{RUNTIME}:/out",
IMAGE,
"mosquitto_passwd",
]
if create:
command.append("-c")
command.extend([f"/out/{path.name}", username])
subprocess.run(
command,
check=True,
input=f"{password}\n{password}\n",
text=True,
)
def _prepare_password_entries(
path: Path,
ingest_user: str,
ingest_password: str,
worker_user: str,
worker_password: str,
) -> None:
_password_entry(
path,
ingest_user,
ingest_password,
create=not path.exists(),
)
_password_entry(path, worker_user, worker_password, create=False)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--initialize",
action="store_true",
help="create a private .env with generated local credentials",
)
parser.add_argument(
"--migrate",
action="store_true",
help="add newly required generated secrets without replacing existing values",
)
parser.add_argument(
"--mqtt-bind-address",
default="127.0.0.1",
help="host IP exposed to telemetry agents",
)
arguments = parser.parse_args()
if arguments.initialize:
_initialize_environment(arguments.mqtt_bind_address)
if arguments.migrate:
_migrate_environment()
values = _environment()
ingest_user = _required(values, "MISSIONCORE_MQTT_INGEST_USER")
ingest_password = _required(values, "MISSIONCORE_MQTT_INGEST_PASSWORD")
worker_user = _identifier(values, "MISSIONCORE_MQTT_WORKER_006_USER")
worker_contour = _identifier(
values,
"MISSIONCORE_MQTT_WORKER_006_CONTOUR",
)
worker_password = _required(values, "MISSIONCORE_MQTT_WORKER_006_PASSWORD")
_required(values, "MISSIONCORE_DB_PASSWORD")
RUNTIME.mkdir(parents=True, exist_ok=True)
password_path = RUNTIME / "passwords"
_prepare_password_entries(
password_path,
ingest_user,
ingest_password,
worker_user,
worker_password,
)
acl_path = RUNTIME / "acl"
acl_path.write_text(
"\n".join(
[
f"user {ingest_user}",
"topic read mission-core/v1/contours/+/agents/+/+",
"topic read $SYS/broker/uptime",
"",
"# Every credential is scoped to one contour and one stable agent.",
f"user {worker_user}",
(
"topic write mission-core/v1/contours/"
f"{worker_contour}/agents/{worker_user}/+"
),
"",
]
),
encoding="utf-8",
)
os.chmod(password_path, 0o600)
os.chmod(acl_path, 0o600)
print("Mosquitto password and ACL files prepared.", flush=True)
if __name__ == "__main__":
main()