Split host setup into explicit entity actions

This commit is contained in:
deytenit committed 2026-11-08 12:39:49 +03:00
1 parent a297e7c71e
commit ba9ef92051
51 files changed
+1023 -275

No files matched your search

+96 -5
View File
@@ -91,12 +91,13 @@ def preflight(*, strict=True):
def package_versions():
architecture = run(["dpkg", "--print-architecture"]).stdout.strip()
result = run(["dpkg-query", "-W", "-f=${binary:Package}\t${Status}\t${Version}\n"])
installed = {}
for line in result.stdout.splitlines():
fields = line.split("\t")
if len(fields) == 3 and fields[1].split()[1:] == ["ok", "installed"]:
installed[fields[0].removesuffix(":amd64")] = fields[2]
installed[fields[0].removesuffix(":" + architecture)] = fields[2]
return installed
@@ -311,6 +312,88 @@ def docker_preflight(installed):
return {**config, **DOCKER_CONFIG}
DOCKER_KEY_SHA256 = "1500c1f56fa9e26b9b8f42452a553675796ade0807cdce11975eb98170b3a570"
LOCAL_DOCKER = ["docker", "--host", "unix:///var/run/docker.sock"]
def _docker_source(arch):
key = Path("/etc/apt/keyrings/apex-docker.asc")
source = Path("/etc/apt/sources.list.d/apex-docker.sources")
content = ("Types: deb\nURIs: https://download.docker.com/linux/debian\nSuites: trixie\n"
f"Components: stable\nArchitectures: {arch}\nSigned-By: {key}\n")
for path in (key, source):
if any(part.is_symlink() for part in (path, *path.parents)):
raise BaselineError(f"Refusing redirected APT path: {path}")
if source.exists() and source.read_text() != content:
raise BaselineError(f"Reconcile existing Docker repository configuration: {source}")
if key.exists():
data = key.read_bytes()
else:
url = "https://download.docker.com/linux/debian/gpg"
with urllib.request.urlopen(url, timeout=30) as response: # noqa: S310
data = response.read(1024 * 1024)
if hashlib.sha256(data).hexdigest() != DOCKER_KEY_SHA256:
raise BaselineError("Docker repository signing key differs from the pinned release key.")
write_managed(key, data)
write_managed(source, content)
def _docker_capabilities(root, *, complete_plugins=False):
capabilities = {"docker_data_root": root}
plugins = {"docker-compose-plugin": ("compose", ["compose", "version", "--short"]),
"docker-buildx-plugin": ("buildx", ["buildx", "version"])}
missing = []
for package, (name, args) in plugins.items():
result = run([*LOCAL_DOCKER, *args], check=False)
if result.returncode or not result.stdout.strip():
missing.append(package)
else:
capabilities[name] = result.stdout.strip()
if missing:
if complete_plugins and "docker-ce" in package_versions():
with service_start_guard():
install_missing(missing)
return _docker_capabilities(root)
raise BaselineError("Existing Docker needs functional plugins: " + ", ".join(missing))
if not root:
raise BaselineError("Unable to verify the local Docker data-root.")
return capabilities
@expected_errors
def apply_docker_policy():
if Path("/usr/bin/pveversion").exists():
raise BaselineError("Docker installation on a Proxmox host requires separate administration.")
release = dict(line.split("=", 1) for line in Path("/etc/os-release").read_text().splitlines()
if "=" in line and not line.startswith("#"))
arch = run(["dpkg", "--print-architecture"]).stdout.strip()
if (release.get("ID", "").strip('"') != "debian" or
release.get("VERSION_ID", "").strip('"') != "13" or arch not in ("amd64", "arm64")):
raise BaselineError("Docker installation supports stock Debian 13 amd64 or arm64.")
info = [*LOCAL_DOCKER, "info", "--format", "{{.DockerRootDir}}"]
if shutil.which("docker"):
current = run(info, check=False)
if current.returncode == 0:
return _docker_capabilities(current.stdout.strip(), complete_plugins=True)
installed = package_versions()
source = Path("/etc/apt/sources.list.d/apex-docker.sources")
if (not source.exists() and
(shutil.which("docker") or set(DOCKER_PACKAGES).intersection(installed))):
raise BaselineError("Existing Docker installation is inactive; reconcile it before retrying.")
config = docker_preflight(installed)
with service_start_guard():
install_missing(("ca-certificates",))
_docker_source(arch)
write_managed("/etc/docker/daemon.json", json.dumps(config, indent=2) + "\n")
install_missing(DOCKER_PACKAGES)
ensure_service("containerd")
ensure_service("docker")
root = run(info).stdout.strip()
if root != DOCKER_CONFIG["data-root"]:
raise BaselineError("Docker started with an unexpected data-root; inspect its configuration.")
return _docker_capabilities(root)
def configure_sources():
write_managed("/etc/apt/sources.list.d/apex-zfs.sources",
"Types: deb\nURIs: https://deb.debian.org/debian\nSuites: trixie\n"
@@ -418,7 +501,7 @@ def repair_bouncer_credentials():
@expected_errors
def apply_base_policy():
"""The bounded configure/base policy: no Docker, storage or enrollment."""
"""Apply the base package, logging, and firewall policy."""
preflight(strict=False)
with service_start_guard(), security_service_guard(BASE_PACKAGES):
install_missing(BASE_PACKAGES)
@@ -478,11 +561,19 @@ if __name__ == "__main__":
import argparse
from .state import ProvisionError, exclusive_lock
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-policy", action="store_true")
scope = parser.add_mutually_exclusive_group()
scope.add_argument("--base-policy", action="store_true")
scope.add_argument("--docker-policy", action="store_true")
args = parser.parse_args()
try:
preflight(strict=not args.base_policy)
preflight(strict=not (args.base_policy or args.docker_policy))
with exclusive_lock("/var/lib/apex-init/init.lock"):
print(json.dumps(apply_base_policy() if args.base_policy else apply_baseline(), sort_keys=True))
if args.docker_policy:
result = apply_docker_policy()
elif args.base_policy:
result = apply_base_policy()
else:
result = apply_baseline()
print(json.dumps(result, sort_keys=True))
except (BaselineError, OSError, ProvisionError) as exc:
parser.exit(1, f"Baseline failed: {exc}\n")
@@ -0,0 +1,120 @@
from contextlib import nullcontext
import hashlib
import io
import json
from pathlib import Path
from subprocess import CompletedProcess
import tempfile
import unittest
from unittest.mock import patch
from provisioning import baseline
class ManualDockerTests(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.root = Path(tmp.name).resolve()
(self.root / "etc").mkdir()
(self.root / "etc/os-release").write_text("ID=debian\nVERSION_ID=13\n")
self.commands = []
self.arch = "arm64"
self.running = False
self.data_root = "/custom/data"
self.missing_buildx = False
self.versions = {}
self.installations = []
def run_command(self, cmd, **kwargs):
self.commands.append(cmd)
if cmd == ["dpkg", "--print-architecture"]:
return CompletedProcess(cmd, 0, self.arch + "\n", "")
if cmd[-1] == "{{.DockerRootDir}}":
return CompletedProcess(cmd, 0 if self.running else 1, self.data_root + "\n", "")
if cmd[-2:] in (["version", "--short"], ["buildx", "version"]):
if self.missing_buildx and cmd[-2:] == ["buildx", "version"]:
if ["docker-buildx-plugin"] not in self.installations:
return CompletedProcess(cmd, 1, "", "plugin missing")
return CompletedProcess(cmd, 0, "installed\n", "")
raise AssertionError(cmd)
def apply(self):
def target(path):
path = Path(path)
return path if self.root in path.parents else self.root / str(path).lstrip("/")
with (patch.object(baseline, "Path", side_effect=target),
patch.object(baseline, "run", side_effect=self.run_command),
patch.object(baseline, "package_versions", return_value=self.versions),
patch.object(baseline.shutil, "which", return_value="/usr/bin/docker" if self.running else None),
patch.object(baseline, "install_missing", side_effect=self.installations.append)):
return baseline.apply_docker_policy()
def test_working_engine_keeps_local_data_root_without_writes(self):
self.running = True
self.versions = {"docker.io": "1"}
self.assertEqual(self.apply()["docker_data_root"], "/custom/data")
self.assertEqual(self.installations, [])
self.assertFalse((self.root / "etc/docker").exists())
def test_pve_refused_before_packages_or_daemon(self):
binary = self.root / "usr/bin/pveversion"
binary.parent.mkdir(parents=True)
binary.touch()
with self.assertRaisesRegex(baseline.BaselineError, "Proxmox"):
self.apply()
self.assertEqual(self.commands, [])
def test_unsupported_architecture_refused(self):
self.arch = "i386"
with self.assertRaisesRegex(baseline.BaselineError, "amd64|arm64"):
self.apply()
self.assertEqual(self.installations, [])
def test_existing_runtime_packages_not_replaced(self):
self.versions = {"containerd": "1.7"}
with self.assertRaisesRegex(baseline.BaselineError, "containerd"):
self.apply()
self.assertEqual(self.installations, [])
def test_fresh_arm64_install_verifies_the_pinned_key_and_starts_local_engine(self):
key = b"test repository signing key"
self.data_root = "/srv/docker"
def start(service):
if service == "docker":
self.running = True
with (patch.object(baseline, "service_start_guard", return_value=nullcontext()),
patch.object(baseline.urllib.request, "urlopen", return_value=io.BytesIO(key)),
patch.object(baseline, "DOCKER_KEY_SHA256", hashlib.sha256(key).hexdigest()),
patch.object(baseline, "ensure_service", side_effect=start)):
self.assertEqual(self.apply()["docker_data_root"], "/srv/docker")
source = (self.root / "etc/apt/sources.list.d/apex-docker.sources").read_text()
self.assertIn("Architectures: arm64", source)
self.assertIn("Signed-By: ", source)
self.assertEqual((self.root / "etc/apt/keyrings/apex-docker.asc").read_bytes(), key)
self.assertEqual(json.loads((self.root / "etc/docker/daemon.json").read_text())["data-root"],
"/srv/docker")
self.assertIn("docker-compose-plugin", self.installations[-1])
self.assertIn("docker-buildx-plugin", self.installations[-1])
self.assertNotIn("ufw", self.installations[-1])
def test_untrusted_signing_key_cannot_publish_apt_source_or_install_docker(self):
with (patch.object(baseline, "service_start_guard", return_value=nullcontext()),
patch.object(baseline.urllib.request, "urlopen", return_value=io.BytesIO(b"wrong key"))):
with self.assertRaisesRegex(baseline.BaselineError, "signing key"):
self.apply()
self.assertFalse((self.root / "etc/apt/sources.list.d/apex-docker.sources").exists())
self.assertFalse((self.root / "etc/docker/daemon.json").exists())
self.assertEqual(self.installations, [("ca-certificates",)])
def test_working_ce_engine_only_installs_the_missing_plugin(self):
self.running = True
self.missing_buildx = True
self.versions = {"docker-ce": "1"}
with patch.object(baseline, "service_start_guard", return_value=nullcontext()):
self.assertEqual(self.apply()["docker_data_root"], "/custom/data")
self.assertEqual(self.installations, [["docker-buildx-plugin"]])
self.assertFalse((self.root / "etc/docker").exists())