Python control-panel package driving the full provisioning flow from a site's site.yaml (docs/05): - provision: ZFS web dataset + ownership, per-site DB + least-priv user, generated .env encrypted to secrets.enc.yaml (SOPS/age), render the profile templates + persist site.yaml, per-customer chrooted SFTP account, docker compose up. - deprovision (gated: data destroyed only with --purge, after a final backup), backup (ZFS snapshot + mariadb-dump), restore (rollback + import), render (preview), list. Design: one command/file runner with a real --dry-run (prints every action, redacts secrets); idempotent steps; Config + Site validation mirroring docs/03; passwords never logged. Modules: cli, config, naming, context, render, runner, zfs, database, secrets, sftp, compose, provision, backup. Plus pyproject (heleosctl entry point), config.example.yaml, an example site, and a README. Tests: 22 pure-logic unit tests (naming, config validation, template render across all profiles + db on/off) — all passing. Full provision and deprovision verified end-to-end in --dry-run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
62 lines
2.1 KiB
Python
62 lines
2.1 KiB
Python
"""Shared-MariaDB operations: create/drop per-site database + least-priv user.
|
|
|
|
SQL is piped over stdin to ``docker exec -i mariadb mariadb`` so the root
|
|
password never appears in the process list; the command itself is logged redacted.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from . import naming
|
|
from .config import Config, Site
|
|
from .errors import CommandError
|
|
from .runner import Runner
|
|
|
|
|
|
def root_password(cfg: Config) -> str:
|
|
val = os.environ.get("MARIADB_ROOT_PASSWORD")
|
|
if val:
|
|
return val
|
|
p = Path(cfg.mariadb_env_file)
|
|
if p.is_file():
|
|
for line in p.read_text(encoding="utf-8").splitlines():
|
|
if line.startswith("MARIADB_ROOT_PASSWORD="):
|
|
return line.split("=", 1)[1].strip()
|
|
raise CommandError(
|
|
"MARIADB_ROOT_PASSWORD not set (env var or mariadb .env at "
|
|
f"{cfg.mariadb_env_file})"
|
|
)
|
|
|
|
|
|
def _exec_sql(runner: Runner, cfg: Config, sql: str) -> None:
|
|
pw = "<redacted>" if runner.dry_run else root_password(cfg)
|
|
runner.run(
|
|
["docker", "exec", "-i", cfg.mariadb_container, "mariadb", "-uroot", f"-p{pw}"],
|
|
input=sql, secret=True,
|
|
)
|
|
|
|
|
|
def create_db_user(runner: Runner, cfg: Config, site: Site, password: str) -> None:
|
|
db = naming.database_name(site.customer, site.site)
|
|
user = naming.database_user(site.customer, site.site)
|
|
sql = (
|
|
f"CREATE DATABASE IF NOT EXISTS `{db}` "
|
|
"CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\n"
|
|
f"CREATE USER IF NOT EXISTS '{user}'@'%' IDENTIFIED BY '{password}';\n"
|
|
f"ALTER USER '{user}'@'%' IDENTIFIED BY '{password}';\n"
|
|
f"GRANT ALL PRIVILEGES ON `{db}`.* TO '{user}'@'%';\n"
|
|
"FLUSH PRIVILEGES;\n"
|
|
)
|
|
_exec_sql(runner, cfg, sql)
|
|
|
|
|
|
def drop_db_user(runner: Runner, cfg: Config, site: Site) -> None:
|
|
db = naming.database_name(site.customer, site.site)
|
|
user = naming.database_user(site.customer, site.site)
|
|
sql = (
|
|
f"DROP USER IF EXISTS '{user}'@'%';\n"
|
|
f"DROP DATABASE IF EXISTS `{db}`;\n"
|
|
"FLUSH PRIVILEGES;\n"
|
|
)
|
|
_exec_sql(runner, cfg, sql)
|