From 62fe113b590e27a023842a83aa11d4c755557b40 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sun, 8 Mar 2026 18:07:56 +0100 Subject: [PATCH] lxc: extract blocked_service_startup() context manager into basedeploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the policy-rc.d install/remove boilerplate into a shared context manager in basedeploy.py so both UnboundDeployer and DovecotDeployer use the same abstraction, and the DNS container's _install_powerdns() inline shell uses the same pattern. DovecotDeployer now wraps its three package installs in blocked_service_startup() to prevent Dovecot from auto-starting on initial install — avoiding bind conflicts on IPv4-only systems. --- cmdeploy/src/cmdeploy/basedeploy.py | 23 +++++++++++++++ cmdeploy/src/cmdeploy/deployers.py | 36 +++++++---------------- cmdeploy/src/cmdeploy/dovecot/deployer.py | 9 ++++-- cmdeploy/src/cmdeploy/lxc/incus.py | 9 ++++++ 4 files changed, 48 insertions(+), 29 deletions(-) diff --git a/cmdeploy/src/cmdeploy/basedeploy.py b/cmdeploy/src/cmdeploy/basedeploy.py index 45654c27..732ed049 100644 --- a/cmdeploy/src/cmdeploy/basedeploy.py +++ b/cmdeploy/src/cmdeploy/basedeploy.py @@ -1,6 +1,7 @@ import importlib.resources import io import os +from contextlib import contextmanager from pyinfra.operations import files, server, systemd @@ -10,6 +11,28 @@ def has_systemd(): return os.path.isdir("/run/systemd/system") +@contextmanager +def blocked_service_startup(): + """Prevent services from auto-starting during package installation. + + Installs a ``/usr/sbin/policy-rc.d`` that exits 101, blocking any + service from being started by the package manager. This avoids bind + conflicts and CPU/RAM spikes during initial setup. The file is removed + when the context exits. + """ + # For documentation about policy-rc.d, see: + # https://people.debian.org/~hmh/invokerc.d-policyrc.d-specification.txt + files.put( + src=get_resource("policy-rc.d"), + dest="/usr/sbin/policy-rc.d", + user="root", + group="root", + mode="755", + ) + yield + files.file("/usr/sbin/policy-rc.d", present=False) + + def get_resource(arg, pkg=__package__): return importlib.resources.files(pkg).joinpath(arg) diff --git a/cmdeploy/src/cmdeploy/deployers.py b/cmdeploy/src/cmdeploy/deployers.py index db952e8b..b259c968 100644 --- a/cmdeploy/src/cmdeploy/deployers.py +++ b/cmdeploy/src/cmdeploy/deployers.py @@ -22,6 +22,7 @@ from .basedeploy import ( Deployer, Deployment, activate_remote_units, + blocked_service_startup, configure_remote_units, get_resource, has_systemd, @@ -148,33 +149,16 @@ class UnboundDeployer(Deployer): self.need_restart = False def install(self): - # Run local DNS resolver `unbound`. - # `resolvconf` takes care of setting up /etc/resolv.conf - # to use 127.0.0.1 as the resolver. + # Run local DNS resolver `unbound`. `resolvconf` takes care of + # setting up /etc/resolv.conf to use 127.0.0.1 as the resolver. - # - # On an IPv4-only system, if unbound is started but not - # configured, it causes subsequent steps to fail to resolve hosts. - # Here, we use policy-rc.d to prevent unbound from starting up - # on initial install. Later, we will configure it and start it. - # - # For documentation about policy-rc.d, see: - # https://people.debian.org/~hmh/invokerc.d-policyrc.d-specification.txt - # - files.put( - src=get_resource("policy-rc.d"), - dest="/usr/sbin/policy-rc.d", - user="root", - group="root", - mode="755", - ) - - apt.packages( - name="Install unbound", - packages=["unbound", "unbound-anchor", "dnsutils"], - ) - - files.file("/usr/sbin/policy-rc.d", present=False) + # On an IPv4-only system, if unbound is started but not configured, + # it causes subsequent steps to fail to resolve hosts. + with blocked_service_startup(): + apt.packages( + name="Install unbound", + packages=["unbound", "unbound-anchor", "dnsutils"], + ) def configure(self): server.shell( diff --git a/cmdeploy/src/cmdeploy/dovecot/deployer.py b/cmdeploy/src/cmdeploy/dovecot/deployer.py index ad4c7c1d..07627888 100644 --- a/cmdeploy/src/cmdeploy/dovecot/deployer.py +++ b/cmdeploy/src/cmdeploy/dovecot/deployer.py @@ -10,6 +10,7 @@ from pyinfra.operations import apt, files, server, systemd from cmdeploy.basedeploy import ( Deployer, activate_remote_units, + blocked_service_startup, configure_remote_units, get_resource, has_systemd, @@ -28,9 +29,11 @@ class DovecotDeployer(Deployer): arch = host.get_fact(Arch) if has_systemd() and "dovecot.service" in host.get_fact(SystemdEnabled): return # already installed and running - _install_dovecot_package("core", arch) - _install_dovecot_package("imapd", arch) - _install_dovecot_package("lmtpd", arch) + + with blocked_service_startup(): + _install_dovecot_package("core", arch) + _install_dovecot_package("imapd", arch) + _install_dovecot_package("lmtpd", arch) def configure(self): configure_remote_units(self.config.mail_domain, self.units) diff --git a/cmdeploy/src/cmdeploy/lxc/incus.py b/cmdeploy/src/cmdeploy/lxc/incus.py index 61abe0f1..baf8e303 100644 --- a/cmdeploy/src/cmdeploy/lxc/incus.py +++ b/cmdeploy/src/cmdeploy/lxc/incus.py @@ -605,9 +605,18 @@ class DNSContainer(Container): systemctl disable --now systemd-resolved 2>/dev/null || true rm -f /etc/resolv.conf echo 'nameserver 9.9.9.9' > /etc/resolv.conf + + # Block automatic service startup during package installation + printf '#!/bin/sh\\nexit 101\\n' > /usr/sbin/policy-rc.d + chmod +x /usr/sbin/policy-rc.d + apt-get -o DPkg::Lock::Timeout=60 update DEBIAN_FRONTEND=noninteractive apt-get install -y \ pdns-server pdns-backend-sqlite3 sqlite3 pdns-recursor dnsutils + + # Remove the startup block + rm /usr/sbin/policy-rc.d + systemctl stop pdns pdns-recursor || true mkdir -p /var/lib/powerdns sqlite3 /var/lib/powerdns/pdns.sqlite3 \