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.
This commit is contained in:
j4n
2026-03-21 15:48:02 +01:00
parent 7409bd3452
commit e687120d96

View File

@@ -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):