refactor: deployer improvements (VM detection, mailboxes dir ensured to be there, proper unbound on ipv4)

This commit is contained in:
holger krekel
2026-04-05 11:08:11 +02:00
parent c257bfca4b
commit 00d723bd6e
4 changed files with 57 additions and 45 deletions

View File

@@ -111,7 +111,6 @@ def run_cmd(args, out):
if ssh_host in ["localhost", "@docker"]: if ssh_host in ["localhost", "@docker"]:
if ssh_host == "@docker": if ssh_host == "@docker":
env["CHATMAIL_NOPORTCHECK"] = "True" env["CHATMAIL_NOPORTCHECK"] = "True"
env["CHATMAIL_NOSYSCTL"] = "True"
cmd = f"{pyinf} @local {deploy_path} -y" cmd = f"{pyinf} @local {deploy_path} -y"
if version.parse(pyinfra.__version__) < version.parse("3"): if version.parse(pyinfra.__version__) < version.parse("3"):

View File

@@ -24,6 +24,7 @@ from .basedeploy import (
Deployer, Deployer,
Deployment, Deployment,
activate_remote_units, activate_remote_units,
blocked_service_startup,
configure_remote_units, configure_remote_units,
get_resource, get_resource,
has_systemd, has_systemd,
@@ -149,33 +150,16 @@ class UnboundDeployer(Deployer):
self.need_restart = False self.need_restart = False
def install(self): def install(self):
# Run local DNS resolver `unbound`. # Run local DNS resolver `unbound`. `resolvconf` takes care of
# `resolvconf` takes care of setting up /etc/resolv.conf # setting up /etc/resolv.conf to use 127.0.0.1 as the resolver.
# to use 127.0.0.1 as the resolver.
# # On an IPv4-only system, if unbound is started but not configured,
# On an IPv4-only system, if unbound is started but not # it causes subsequent steps to fail to resolve hosts.
# configured, it causes subsequent steps to fail to resolve hosts. with blocked_service_startup():
# Here, we use policy-rc.d to prevent unbound from starting up apt.packages(
# on initial install. Later, we will configure it and start it. name="Install unbound",
# packages=["unbound", "unbound-anchor", "dnsutils"],
# 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)
def configure(self): def configure(self):
server.shell( server.shell(
@@ -474,8 +458,9 @@ class ChatmailDeployer(Deployer):
("iroh", None, None), ("iroh", None, None),
] ]
def __init__(self, mail_domain): def __init__(self, config):
self.mail_domain = mail_domain self.config = config
self.mail_domain = config.mail_domain
def install(self): def install(self):
files.put( files.put(
@@ -500,6 +485,16 @@ class ChatmailDeployer(Deployer):
) )
def configure(self): 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. # This file is used by auth proxy.
# https://wiki.debian.org/EtcMailName # https://wiki.debian.org/EtcMailName
server.shell( 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) tls_deployer = get_tls_deployer(config, mail_domain)
all_deployers = [ all_deployers = [
ChatmailDeployer(mail_domain), ChatmailDeployer(config),
LegacyRemoveDeployer(), LegacyRemoveDeployer(),
FiltermailDeployer(), FiltermailDeployer(),
JournaldDeployer(), JournaldDeployer(),

View File

@@ -1,11 +1,10 @@
import io import io
import os
import urllib.request import urllib.request
from chatmaild.config import Config from chatmaild.config import Config
from pyinfra import host from pyinfra import host
from pyinfra.facts.deb import DebPackages 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 pyinfra.operations import apt, files, server, systemd
from cmdeploy.basedeploy import ( from cmdeploy.basedeploy import (
@@ -128,7 +127,18 @@ def _download_dovecot_package(package: str, arch: str):
return deb_filename 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.""" """Configures Dovecot IMAP server."""
need_restart = False need_restart = False
daemon_reload = 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/ # as per https://doc.dovecot.org/2.3/configuration_manual/os/
# it is recommended to set the following inotify limits # it is recommended to set the following inotify limits
if not os.environ.get("CHATMAIL_NOSYSCTL"): can_modify = _can_set_inotify_limits()
for name in ("max_user_instances", "max_user_watches"): for name in ("max_user_instances", "max_user_watches"):
key = f"fs.inotify.{name}" key = f"fs.inotify.{name}"
if host.get_fact(Sysctl)[key] > 65535: value = host.get_fact(Sysctl)[key]
# Skip updating limits if already sufficient if value > 65534:
# (enables running in incus containers where sysctl readonly) continue
continue if not can_modify:
server.sysctl( print(
name=f"Change {key}", "\n!!!! refusing to attempt sysctl setting in containers\n"
key=key, f"!!!! dovecot: sysctl {key!r}={value}, should be >65534 for production setups\n"
value=65535, "!!!!"
persist=True,
) )
continue
server.sysctl(
name=f"Change {key}",
key=key,
value=65535,
persist=True,
)
timezone_env = files.line( timezone_env = files.line(
name="Set TZ environment variable", name="Set TZ environment variable",

View File

@@ -18,6 +18,8 @@ def openssl_selfsigned_args(domain, cert_path, key_path, days=36500):
"-keyout", str(key_path), "-keyout", str(key_path),
"-out", str(cert_path), "-out", str(cert_path),
"-subj", f"/CN={domain}", "-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", "extendedKeyUsage=serverAuth,clientAuth",
"-addext", "-addext",
f"subjectAltName=DNS:{domain},DNS:www.{domain},DNS:mta-sts.{domain}", f"subjectAltName=DNS:{domain},DNS:www.{domain},DNS:mta-sts.{domain}",