From e687120d96b4a6a66cdd146dbe0f9855dd6035b0 Mon Sep 17 00:00:00 2001 From: j4n Date: Sat, 21 Mar 2026 15:48:02 +0100 Subject: [PATCH] fix(cmdeploy): Install dovecot .deb packages atomically Since change 635ac7 we try to install Dovecot, even if it is already running, which fails Dovecot upgrades fail when the installed version differs from the target because dovecot-imapd/lmtpd dependencies on dovecot-core: packages are installed one at a time via apt.deb(), i.e. `dpkg -i`, and dpkg cannot satisfy them dependencies: ``` dpkg: dependency problems prevent configuration of dovecot-imapd: dovecot-imapd depends on dovecot-core (= 1:2.3.21+dfsg1-3); however: Version of dovecot-core on system is 1:2.3.21.1+dfsg1-1~bpo12+1. ``` Split _install_dovecot_package into _download_dovecot_package (download only, return path) and a single server.shell call that passes all .deb files to dpkg -i together. Uses the same 3-step pattern as pyinfra's apt.deb: tolerant first dpkg -i, apt-get --fix-broken, then final dpkg -i to fail if there are still errors. --- cmdeploy/src/cmdeploy/dovecot/deployer.py | 27 +++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/cmdeploy/src/cmdeploy/dovecot/deployer.py b/cmdeploy/src/cmdeploy/dovecot/deployer.py index 7a36d0a1..b2d0b95f 100644 --- a/cmdeploy/src/cmdeploy/dovecot/deployer.py +++ b/cmdeploy/src/cmdeploy/dovecot/deployer.py @@ -38,9 +38,21 @@ class DovecotDeployer(Deployer): def install(self): arch = host.get_fact(Arch) with blocked_service_startup(): - _install_dovecot_package("core", arch) - _install_dovecot_package("imapd", arch) - _install_dovecot_package("lmtpd", arch) + debs = [] + for pkg in ("core", "imapd", "lmtpd"): + deb = _download_dovecot_package(pkg, arch) + if deb: + debs.append(deb) + if debs: + deb_list = " ".join(debs) + server.shell( + name="Install dovecot packages", + commands=[ + f"dpkg --force-confdef --force-confold -i {deb_list} 2> /dev/null || true", + "DEBIAN_FRONTEND=noninteractive apt-get -y --fix-broken install", + f"dpkg --force-confdef --force-confold -i {deb_list}", + ], + ) def configure(self): configure_remote_units(self.config.mail_domain, self.units) @@ -73,7 +85,8 @@ def _pick_url(primary, fallback): return fallback -def _install_dovecot_package(package: str, arch: str): +def _download_dovecot_package(package: str, arch: str): + """Download a dovecot .deb if needed, return its path (or None).""" arch = "amd64" if arch == "x86_64" else arch arch = "arm64" if arch == "aarch64" else arch @@ -81,11 +94,11 @@ def _install_dovecot_package(package: str, arch: str): sha256 = DOVECOT_SHA256.get((package, arch)) if sha256 is None: apt.packages(packages=[pkg_name]) - return + return None installed_versions = host.get_fact(DebPackages).get(pkg_name, []) if DOVECOT_VERSION in installed_versions: - return + return None url_version = DOVECOT_VERSION.replace("+", "%2B") deb_base = f"{pkg_name}_{url_version}_{arch}.deb" @@ -102,7 +115,7 @@ def _install_dovecot_package(package: str, arch: str): cache_time=60 * 60 * 24 * 365 * 10, # never redownload the package ) - apt.deb(name=f"Install {pkg_name}", src=deb_filename) + return deb_filename def _configure_dovecot(config: Config, debug: bool = False) -> (bool, bool):