367 lines
14 KiB
Python
367 lines
14 KiB
Python
"""Explicit ordinary PostgreSQL initialization; no schema migration or server start."""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import stat
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from . import configuration, dependencies, dependency_install, install, preflight, service_install
|
|
from .policy import DeliveryError, canonical, digest, fields, read_json, sha256
|
|
from .services import python_environment
|
|
|
|
UNIT = "kf-postgres-initialize.service"
|
|
DATA = Path(dependencies.STORAGE["postgres"]["path"]) / "data"
|
|
RECEIPT = dependency_install.ROOT / "postgres.initialization.json"
|
|
CREDENTIAL = Path("/run/credentials") / UNIT / "bootstrap"
|
|
|
|
|
|
def validate_password(contents: bytes) -> None:
|
|
if re.fullmatch(rb"[0-9a-f]{64}\n", contents) is None:
|
|
raise DeliveryError("explicit 256-bit hexadecimal bootstrap password required")
|
|
|
|
|
|
def cluster_identity() -> dict:
|
|
root = DATA.lstat()
|
|
version = DATA / "PG_VERSION"
|
|
info = version.lstat()
|
|
if (
|
|
not stat.S_ISDIR(root.st_mode)
|
|
or (root.st_uid, root.st_gid, stat.S_IMODE(root.st_mode)) != (1203, 1203, 0o700)
|
|
or not stat.S_ISREG(info.st_mode)
|
|
or info.st_nlink != 1
|
|
or (info.st_uid, info.st_gid, stat.S_IMODE(info.st_mode)) != (1203, 1203, 0o600)
|
|
or info.st_size != 3
|
|
or version.read_bytes() != b"18\n"
|
|
):
|
|
raise DeliveryError("initialized PostgreSQL identity/version differs")
|
|
return {"device": root.st_dev, "inode": root.st_ino, "major_version": 18, "uid": 1203}
|
|
|
|
|
|
def validate_receipt(value: dict) -> dict:
|
|
fields(
|
|
value,
|
|
{
|
|
"schema_version",
|
|
"source_sha",
|
|
"configuration_id",
|
|
"cluster_identity",
|
|
"bootstrap_sha256",
|
|
"initialized",
|
|
"migrated",
|
|
"services_started",
|
|
"execution_verified",
|
|
},
|
|
"database initialization receipt",
|
|
)
|
|
if (
|
|
type(value["schema_version"]) is not int
|
|
or value["schema_version"] != 1
|
|
or not isinstance(value["source_sha"], str)
|
|
or not re.fullmatch(r"[0-9a-f]{40}", value["source_sha"])
|
|
or not isinstance(value["configuration_id"], str)
|
|
or not re.fullmatch(r"cfg-[0-9a-f]{24}", value["configuration_id"])
|
|
or value["initialized"] is not True
|
|
or any(
|
|
value[key] is not False
|
|
for key in ("migrated", "services_started", "execution_verified")
|
|
)
|
|
):
|
|
raise DeliveryError("initialization receipt binding differs")
|
|
sha256(value["bootstrap_sha256"])
|
|
validate_cluster_identity(value["cluster_identity"])
|
|
return value
|
|
|
|
|
|
def validate_cluster_identity(value: dict) -> dict:
|
|
identity = fields(value, {"device", "inode", "major_version", "uid"}, "cluster identity")
|
|
if (
|
|
any(type(identity[key]) is not int or identity[key] < 1 for key in identity)
|
|
or identity["major_version"] != 18
|
|
or identity["uid"] != 1203
|
|
):
|
|
raise DeliveryError("recorded PostgreSQL cluster identity differs")
|
|
return identity
|
|
|
|
|
|
def run_in_unit() -> dict:
|
|
observed = dependencies.check_runtime("postgres", initializing=True)
|
|
interfaces = Path("/proc/net/dev").read_text().splitlines()[2:]
|
|
if {line.split(":", 1)[0].strip() for line in interfaces} != {"lo"}:
|
|
raise DeliveryError("network-isolated database initializer required")
|
|
credential = CREDENTIAL.lstat()
|
|
if (
|
|
not stat.S_ISREG(credential.st_mode)
|
|
or credential.st_nlink != 1
|
|
or credential.st_uid not in {0, 1203}
|
|
or stat.S_IMODE(credential.st_mode) not in {0o400, 0o600}
|
|
or credential.st_size != 65
|
|
):
|
|
raise DeliveryError("private database initialization credential required")
|
|
validate_password(CREDENTIAL.read_bytes())
|
|
if DATA.exists() or DATA.is_symlink():
|
|
raise DeliveryError("database initializer refuses any existing target")
|
|
preflight.protected(Path("/usr/bin/initdb"))
|
|
env = {"PATH": "/usr/bin:/bin", "LC_ALL": "C.UTF-8", "HOME": "/nonexistent"}
|
|
version = subprocess.run(
|
|
["/usr/bin/initdb", "--version"], capture_output=True, timeout=5, env=env
|
|
)
|
|
if version.returncode or version.stdout.strip() != b"initdb (PostgreSQL) 18.6":
|
|
raise DeliveryError("database initializer tool version differs")
|
|
result = subprocess.run(
|
|
[
|
|
"/usr/bin/initdb",
|
|
"--pgdata=" + str(DATA),
|
|
"--username=postgres",
|
|
"--pwfile=" + str(CREDENTIAL),
|
|
"--auth-host=scram-sha-256",
|
|
"--auth-local=scram-sha-256",
|
|
"--encoding=UTF8",
|
|
"--locale=C.UTF-8",
|
|
"--data-checksums",
|
|
"--no-clean",
|
|
"--no-instructions",
|
|
"--set=shared_buffers=64MB",
|
|
"--set=max_connections=32",
|
|
"--set=work_mem=1MB",
|
|
"--set=maintenance_work_mem=32MB",
|
|
"--set=max_wal_senders=0",
|
|
"--set=listen_addresses=",
|
|
],
|
|
env=env,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
timeout=165,
|
|
)
|
|
if result.returncode:
|
|
raise DeliveryError("ordinary database initialization failed; partial data retained")
|
|
return {
|
|
**observed,
|
|
"initialized": True,
|
|
"cluster_identity": cluster_identity(),
|
|
"services_started": False,
|
|
}
|
|
|
|
|
|
def status() -> dict:
|
|
return dict(
|
|
line.split("=", 1)
|
|
for line in service_install.systemctl(
|
|
"show",
|
|
UNIT,
|
|
"--property=LoadState,ActiveState,SubState,MainPID,Result,Description,Transient",
|
|
).splitlines()
|
|
if "=" in line
|
|
)
|
|
|
|
|
|
def initialize(password_file: Path, password_sha: str) -> dict:
|
|
if os.getuid() != 0 or os.geteuid() != 0:
|
|
raise DeliveryError("explicit database initialization requires root operator")
|
|
preflight.protected(password_file)
|
|
configuration.private(password_file)
|
|
if (
|
|
not re.fullmatch(r"/[A-Za-z0-9_./-]+", str(password_file))
|
|
or password_file.stat().st_size != 65
|
|
or digest(password_file) != sha256(password_sha)
|
|
):
|
|
raise DeliveryError("private database bootstrap fingerprint/path differs")
|
|
validate_password(password_file.read_bytes())
|
|
selected, original = service_install.current_receipt()
|
|
payload = install.RELEASES / selected["release_id"] / "payload"
|
|
for name in ("dependencies.py", "dependency_install.py", "dependency_initialize.py"):
|
|
shipped = payload / "delivery/scripts/release" / name
|
|
preflight.protected(shipped)
|
|
if digest(shipped) != digest(Path(__file__).parent / name):
|
|
raise DeliveryError("database initializer differs from selected delivery")
|
|
dependency_install.require_stopped()
|
|
dependency_install.check_accounts()
|
|
dependencies.storage_observation("postgres")
|
|
if dependency_install.known_link(
|
|
service_install.SYSTEM_UNITS / "kf-postgres.service"
|
|
) != dependency_install.ROOT / (selected["release_id"] + "." + selected["configuration_id"]):
|
|
raise DeliveryError("database dependency snapshot is not the selected binding")
|
|
current = status()
|
|
if (
|
|
current.get("LoadState") != "not-found"
|
|
or current.get("ActiveState") != "inactive"
|
|
or current.get("MainPID") != "0"
|
|
):
|
|
raise DeliveryError("unknown or residual database initialization unit")
|
|
if RECEIPT.exists() or RECEIPT.is_symlink():
|
|
preflight.protected(RECEIPT)
|
|
configuration.private(RECEIPT)
|
|
recorded = validate_receipt(read_json(RECEIPT, 8192))
|
|
if recorded["bootstrap_sha256"] != password_sha or canonical(
|
|
recorded["cluster_identity"]
|
|
) != canonical(cluster_identity()):
|
|
raise DeliveryError(
|
|
"database initialization identity/credential changed; explicit recovery required"
|
|
)
|
|
return {
|
|
"initialized": True,
|
|
"reused": True,
|
|
"credentials_authenticated": False,
|
|
"migrated": False,
|
|
"services_started": False,
|
|
"execution_verified": False,
|
|
}
|
|
if DATA.exists() or DATA.is_symlink():
|
|
raise DeliveryError("unrecorded or partial database target; explicit recovery required")
|
|
description = (
|
|
"KF PostgreSQL initialization "
|
|
+ original["source_sha"]
|
|
+ "."
|
|
+ selected["configuration_id"]
|
|
)
|
|
command = [
|
|
"/usr/bin/systemd-run",
|
|
"--quiet",
|
|
"--unit=" + UNIT,
|
|
"--description=" + description,
|
|
"--slice=kfonlinejudge.slice",
|
|
]
|
|
properties = {
|
|
"Type": "exec",
|
|
"RemainAfterExit": "yes",
|
|
"User": "postgres",
|
|
"Group": "postgres",
|
|
"UMask": "0077",
|
|
"MemoryMax": "512M",
|
|
"MemorySwapMax": "0",
|
|
"TasksMax": "64",
|
|
"CPUQuota": "100%",
|
|
"LimitCORE": "0",
|
|
"LimitFSIZE": "256M",
|
|
"LimitNOFILE": "256",
|
|
"LimitSTACK": "16M",
|
|
"RuntimeMaxSec": "180",
|
|
"TimeoutStartSec": "10",
|
|
"TimeoutStopSec": "15",
|
|
"Restart": "no",
|
|
"KillMode": "control-group",
|
|
"NoNewPrivileges": "yes",
|
|
"CapabilityBoundingSet": "",
|
|
"AmbientCapabilities": "",
|
|
"ProtectSystem": "strict",
|
|
"ProtectHome": "yes",
|
|
"ProtectControlGroups": "yes",
|
|
"PrivateDevices": "yes",
|
|
"PrivateTmp": "yes",
|
|
"PrivateNetwork": "yes",
|
|
"RestrictAddressFamilies": "AF_UNIX",
|
|
"ReadWritePaths": dependencies.STORAGE["postgres"]["path"],
|
|
"RequiresMountsFor": dependencies.STORAGE["postgres"]["path"],
|
|
"LoadCredential": "bootstrap:" + str(password_file),
|
|
}
|
|
for key, value in properties.items():
|
|
command += ["--property=" + key + "=" + value]
|
|
command += [
|
|
"/usr/bin/env",
|
|
"-i",
|
|
"PATH=/usr/bin:/bin",
|
|
"LC_ALL=C.UTF-8",
|
|
*python_environment(payload).split(),
|
|
str(payload / "python/bin/python3.13"),
|
|
"-s",
|
|
"-P",
|
|
"-m",
|
|
"scripts.release.dependency_initialize",
|
|
"--run-in-unit",
|
|
]
|
|
dispatched = False
|
|
try:
|
|
dispatched = True
|
|
result = subprocess.run(
|
|
command,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
timeout=15,
|
|
env={"PATH": "/usr/bin:/bin", "LC_ALL": "C"},
|
|
)
|
|
if result.returncode:
|
|
raise DeliveryError("database initialization dispatch failed")
|
|
deadline = time.monotonic() + 185
|
|
while True:
|
|
actual = status()
|
|
if actual.get("Description") != description or actual.get("Transient") != "yes":
|
|
raise DeliveryError("database initialization unit ownership differs")
|
|
if actual.get("SubState") == "exited" and actual.get("ActiveState") == "active":
|
|
break
|
|
if (
|
|
actual.get("ActiveState") not in {"active", "activating"}
|
|
or time.monotonic() >= deadline
|
|
):
|
|
raise DeliveryError(
|
|
"database initialization failed/expired; partial state retained"
|
|
)
|
|
time.sleep(0.5)
|
|
if actual.get("Result") != "success" or actual.get("MainPID") != "0":
|
|
raise DeliveryError("database initialization unit failed; partial state retained")
|
|
recorded = validate_receipt(
|
|
{
|
|
"schema_version": 1,
|
|
"source_sha": original["source_sha"],
|
|
"configuration_id": selected["configuration_id"],
|
|
"cluster_identity": cluster_identity(),
|
|
"bootstrap_sha256": password_sha,
|
|
"initialized": True,
|
|
"migrated": False,
|
|
"services_started": False,
|
|
"execution_verified": False,
|
|
}
|
|
)
|
|
if install.selection() != selected:
|
|
raise DeliveryError("selected binding changed during database initialization")
|
|
install.exclusive_file(RECEIPT, canonical(recorded), 0o600)
|
|
finally:
|
|
if dispatched:
|
|
actual = status()
|
|
if actual.get("LoadState") != "not-found":
|
|
if actual.get("Description") != description or actual.get("Transient") != "yes":
|
|
raise DeliveryError("foreign initialization unit; refused cleanup")
|
|
service_install.systemctl("stop", UNIT)
|
|
actual = status()
|
|
if actual.get("ActiveState") != "inactive" or actual.get("MainPID") != "0":
|
|
raise DeliveryError("database initialization cleanup unconfirmed")
|
|
return {
|
|
"initialized": True,
|
|
"reused": False,
|
|
"credentials_authenticated": False,
|
|
"migrated": False,
|
|
"services_started": False,
|
|
"execution_verified": False,
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Explicit bounded PostgreSQL initialization")
|
|
parser.add_argument("--password-file", type=Path)
|
|
parser.add_argument("--password-sha256")
|
|
parser.add_argument("--run-in-unit", action="store_true")
|
|
args = parser.parse_args()
|
|
try:
|
|
if args.run_in_unit:
|
|
if args.password_file is not None or args.password_sha256 is not None:
|
|
raise DeliveryError("unexpected initialization unit override")
|
|
result = run_in_unit()
|
|
else:
|
|
if args.password_file is None or args.password_sha256 is None:
|
|
raise DeliveryError("explicit private bootstrap inputs required")
|
|
result = initialize(args.password_file, args.password_sha256)
|
|
print(json.dumps(result, sort_keys=True))
|
|
except (OSError, ValueError, subprocess.SubprocessError) as error:
|
|
parser.exit(
|
|
1,
|
|
"Database initialization refused: "
|
|
+ (str(error) if isinstance(error, DeliveryError) else type(error).__name__)
|
|
+ "\n",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|