From 00d723bd6e24a3cc0dfbc0277a905c91c775c5ac Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sun, 5 Apr 2026 11:08:11 +0200 Subject: [PATCH] refactor: deployer improvements (VM detection, mailboxes dir ensured to be there, proper unbound on ipv4) --- cmdeploy/src/cmdeploy/cmdeploy.py | 1 - cmdeploy/src/cmdeploy/deployers.py | 53 +++++++++----------- cmdeploy/src/cmdeploy/dovecot/deployer.py | 46 +++++++++++------ cmdeploy/src/cmdeploy/selfsigned/deployer.py | 2 + 4 files changed, 57 insertions(+), 45 deletions(-) diff --git a/cmdeploy/src/cmdeploy/cmdeploy.py b/cmdeploy/src/cmdeploy/cmdeploy.py index aace1693..51ccd3f4 100644 --- a/cmdeploy/src/cmdeploy/cmdeploy.py +++ b/cmdeploy/src/cmdeploy/cmdeploy.py @@ -111,7 +111,6 @@ def run_cmd(args, out): if ssh_host in ["localhost", "@docker"]: if ssh_host == "@docker": env["CHATMAIL_NOPORTCHECK"] = "True" - env["CHATMAIL_NOSYSCTL"] = "True" cmd = f"{pyinf} @local {deploy_path} -y" if version.parse(pyinfra.__version__) < version.parse("3"): diff --git a/cmdeploy/src/cmdeploy/deployers.py b/cmdeploy/src/cmdeploy/deployers.py index 3d9dad73..3b2e21d9 100644 --- a/cmdeploy/src/cmdeploy/deployers.py +++ b/cmdeploy/src/cmdeploy/deployers.py @@ -24,6 +24,7 @@ from .basedeploy import ( Deployer, Deployment, activate_remote_units, + blocked_service_startup, configure_remote_units, get_resource, has_systemd, @@ -149,33 +150,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( @@ -474,8 +458,9 @@ class ChatmailDeployer(Deployer): ("iroh", None, None), ] - def __init__(self, mail_domain): - self.mail_domain = mail_domain + def __init__(self, config): + self.config = config + self.mail_domain = config.mail_domain def install(self): files.put( @@ -500,6 +485,16 @@ class ChatmailDeployer(Deployer): ) def configure(self): + # metadata crashes if the mailboxes dir does not exist + files.directory( + name="Ensure vmail mailbox directory exists", + path=str(self.config.mailboxes_dir), + user="vmail", + group="vmail", + mode="700", + present=True, + ) + # This file is used by auth proxy. # https://wiki.debian.org/EtcMailName server.shell( @@ -629,7 +624,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) - tls_deployer = get_tls_deployer(config, mail_domain) all_deployers = [ - ChatmailDeployer(mail_domain), + ChatmailDeployer(config), LegacyRemoveDeployer(), FiltermailDeployer(), JournaldDeployer(), diff --git a/cmdeploy/src/cmdeploy/dovecot/deployer.py b/cmdeploy/src/cmdeploy/dovecot/deployer.py index 3d818813..99952da6 100644 --- a/cmdeploy/src/cmdeploy/dovecot/deployer.py +++ b/cmdeploy/src/cmdeploy/dovecot/deployer.py @@ -1,11 +1,10 @@ import io -import os import urllib.request from chatmaild.config import Config from pyinfra import host from pyinfra.facts.deb import DebPackages -from pyinfra.facts.server import Arch, Sysctl +from pyinfra.facts.server import Arch, Command, Sysctl from pyinfra.operations import apt, files, server, systemd from cmdeploy.basedeploy import ( @@ -128,7 +127,18 @@ def _download_dovecot_package(package: str, arch: str): return deb_filename -def _configure_dovecot(config: Config, debug: bool = False) -> (bool, bool): +def _can_set_inotify_limits() -> bool: + is_container = ( + host.get_fact( + Command, + "systemd-detect-virt --container --quiet 2>/dev/null && echo yes || true", + ) + == "yes" + ) + return not is_container + + +def _configure_dovecot(config: Config, debug: bool = False) -> tuple[bool, bool]: """Configures Dovecot IMAP server.""" need_restart = False daemon_reload = False @@ -163,19 +173,25 @@ def _configure_dovecot(config: Config, debug: bool = False) -> (bool, bool): # as per https://doc.dovecot.org/2.3/configuration_manual/os/ # it is recommended to set the following inotify limits - if not os.environ.get("CHATMAIL_NOSYSCTL"): - for name in ("max_user_instances", "max_user_watches"): - key = f"fs.inotify.{name}" - if host.get_fact(Sysctl)[key] > 65535: - # Skip updating limits if already sufficient - # (enables running in incus containers where sysctl readonly) - continue - server.sysctl( - name=f"Change {key}", - key=key, - value=65535, - persist=True, + can_modify = _can_set_inotify_limits() + for name in ("max_user_instances", "max_user_watches"): + key = f"fs.inotify.{name}" + value = host.get_fact(Sysctl)[key] + if value > 65534: + continue + if not can_modify: + print( + "\n!!!! refusing to attempt sysctl setting in containers\n" + f"!!!! dovecot: sysctl {key!r}={value}, should be >65534 for production setups\n" + "!!!!" ) + continue + server.sysctl( + name=f"Change {key}", + key=key, + value=65535, + persist=True, + ) timezone_env = files.line( name="Set TZ environment variable", diff --git a/cmdeploy/src/cmdeploy/selfsigned/deployer.py b/cmdeploy/src/cmdeploy/selfsigned/deployer.py index 0faff5e8..7f6d5015 100644 --- a/cmdeploy/src/cmdeploy/selfsigned/deployer.py +++ b/cmdeploy/src/cmdeploy/selfsigned/deployer.py @@ -18,6 +18,8 @@ def openssl_selfsigned_args(domain, cert_path, key_path, days=36500): "-keyout", str(key_path), "-out", str(cert_path), "-subj", f"/CN={domain}", + # Mark as end-entity cert so it cannot be used as a CA to sign others. + "-addext", "basicConstraints=critical,CA:FALSE", "-addext", "extendedKeyUsage=serverAuth,clientAuth", "-addext", f"subjectAltName=DNS:{domain},DNS:www.{domain},DNS:mta-sts.{domain}",