Compare commits

..

47 Commits

Author SHA1 Message Date
cliffmccarthy
2522fb676c docs: Remove section about use of objects 2025-11-13 07:40:40 -06:00
cliffmccarthy
01cd634be6 docs: Update cmdeploy architecture details
- Revised cmdeploy documentation in doc/source/overview.rst to reflect
  the recent revisions to the Deployer interface.
2025-11-12 23:24:14 -06:00
cliffmccarthy
e20226f331 refactor: Simplify interface to Deployer.install()
- In the current code, the only class using the interface that sets
  need_restart() from the return value of the install() method was
  IrohDeployer.  That interface was created when the install method
  was a static method, but now it is an instance method with access to
  'self'.  Therefore, we don't need to pass anything up to the caller
  to have them set the attribute, we can just set it.
- Revised IrohDeployer.install() to set self.need_restart directly,
  rather than returning a value.
- Revised Deployment.install() to ignore the return value of the
  deployers' install() methods.
- need_restart is still present in the base Deployer class to ensure
  that it is always defined, even when classes do not set it in a
  constructor.  Apart from this initialization for convenience, there
  is no longer any specific exposure of need_restart in the interface
  of the Deployer class.
- In general, install() methods should use 'self' as little as
  possible, preferably not at all.  In particular, install() methods
  should never depend on "config" data, such as the config dictionary
  in self.config or specific values like self.mail_domain.  This
  ensures that these methods can be used to perform generic
  installation operations that are applicable across multiple relay
  deployments, and therefore can be called in the process of building
  a general-purpose container image.
2025-11-12 19:16:51 -06:00
cliffmccarthy
354418c877 refactor: Pass all constructor arguments by position
- The constructor arguments do not have default values; they are all
  required.  Revised deploy_chatmail() to pass them by position rather
  than name, so that the caller is not coupled to the names of the
  arguments inside the method definition.
2025-11-12 19:16:51 -06:00
cliffmccarthy
e98b142585 style: Formatting revisions 2025-11-12 19:16:51 -06:00
cliffmccarthy
2ca4dd5f30 refactor: Revise AcmetoolDeployer for new Deployer interface 2025-11-12 19:16:51 -06:00
holger krekel
47c14f0b70 use a Deployer for setting the remote git hash 2025-11-12 19:16:51 -06:00
holger krekel
258cd9f4c3 strike unneccessary *,** argument flexibility 2025-11-12 19:16:51 -06:00
holger krekel
282b4965a2 remove static method and Make Deployer instances not set any default state 2025-11-12 19:16:51 -06:00
holger krekel
6a1f7543a5 now that Deployer class is clean and not mixed with what is in Deployment, use the simpler "install", "configure" and "activate" namings instead of *_impl 2025-11-12 19:16:51 -06:00
holger krekel
b0f247a41f further reduce indirections for staged install 2025-11-12 19:16:51 -06:00
holger krekel
66daf3003b simplify required_users configuration (a method is not needed for now) 2025-11-12 19:16:51 -06:00
holger krekel
4a154b0a2c remove indirection with "stages" 2025-11-12 19:16:51 -06:00
holger krekel
8557abacda strike unnccessary deployer variables 2025-11-12 19:16:51 -06:00
cliffmccarthy
415dc15e49 refactor: Move doveauth out of ChatmailVenvDeployer
- Revised DovecotDeployer to use _configure_remote_units() and
  _activate_remote_units() to deploy doveauth.  This keeps the
  Dovecot-related services in a single deployer class, leaving only
  services that are part of the chatmail project in
  ChatmailVenvDeployer.
- Removed doveauth from the unit list in ChatmailVenvDeployer.
2025-11-12 19:16:51 -06:00
cliffmccarthy
1166877eef refactor: Move echobot out of ChatmailVenvDeployer
- Revised EchobotDeployer to use _configure_remote_units() and
  _activate_remote_units().  The 'activate' stage of
  ChatmailVenvDeployer was unconditionally restarting the service
  every time, so EchobotDeployer no longer needs to depend on the
  was_restarted attributes of the postfix and dovecot deployers in an
  attempt to avoid restarting it; we can just handle the unconditional
  restart in EchobotDeployer.activate_impl().
- Removed echobot from the unit list in ChatmailVenvDeployer.
- Removed now-unused was_restarted attribute from PostfixDeployer and
  DovecotDeployer.
2025-11-12 19:16:51 -06:00
cliffmccarthy
12884e0caf refactor: Move turnserver out of ChatmailVenvDeployer
- Revised TurnDeployer to use _configure_remote_units() and
  _activate_remote_units().  This class no longer uses need_restart
  and daemon_reload attributes to keep track of state.  The activate
  stage of ChatmailVenvDeployer was unconditionally restarting the
  service every time, so we don't need to keep track of extra state in
  an attempt to avoid restarting it; we can just handle the
  unconditional restart in TurnDeployer.activate_impl().
- Removed turnserver from the unit list in ChatmailVenvDeployer.
2025-11-12 19:16:51 -06:00
cliffmccarthy
897d4f161b refactor: Move unit list to ChatmailVenvDeployer
- Split _configure_remote_venv_with_chatmaild() into two functions.
  _configure_remote_venv_with_chatmaild() handles details specific to
  the "venv", while the new _configure_remote_units() is a more
  general function that is applicable to several services.
- Renamed _activate_remote_venv_with_chatmaild() to
  _activate_remote_units() because doesn't have anything
  venv-specific.
- Removed list of units from helper functions (where it appeared
  twice); moved it to ChatmailVenvDeployer, where its is passed as an
  argument to _configure_remote_units() and _activate_remote_units().
2025-11-12 19:16:51 -06:00
cliffmccarthy
8afbea9b31 chore: Add CHANGELOG.md entry for cmdeploy refactor 2025-11-12 19:16:51 -06:00
cliffmccarthy
ca1bd77d37 feat: Reorder deployers
- Moved fcgiwrap before nginx.
- Exchanged order of turn and unbound.
- Moved journald as early as possible.
- Suggested in review by missytake.
2025-11-12 19:16:51 -06:00
cliffmccarthy
b2de410335 feat: Remove obs-home-deltachat.gpg
- We don't install Dovecot from OBS anymore.
- Removed files.put() that creates
  /etc/apt/keyrings/obs-home-deltachat.gpg; replaced this with a
  files.file() that sets present=False to remove the file from any
  existing installations where it already has been installed.
- Removed now-unused obs-home-deltachat.gpg file.
- Clarified description of sources.list operation.
- Suggested in review by missytake and hpk42.
2025-11-12 19:16:51 -06:00
cliffmccarthy
656cc71f08 fix: Block unbound from starting up on install
- On an IPv4-only system, if unbound is started but not configured, it
  causes subsequent steps to fail to resolve hosts.
- Revised UnboundDeployer.install_impl() to use policy-rc.d to prevent
  the service from starting when installed.  This is the same
  mechanism used to keep nginx from starting on install.
2025-11-12 19:16:51 -06:00
cliffmccarthy
181b7a6d5b docs: Add architectural information about deployer classes
- Updated overview.rst to describe the Deployer class hierarchy and
  the motivations behind it.
2025-11-12 19:16:51 -06:00
cliffmccarthy
0273768c0d refactor: Call install, configure, and activate methods in loops
- Revised deploy_chatmail() to use all_deployers to call the
  install(), configure(), and activate() methods on all the deployers,
  rather than listing them explicitly in the code.
2025-11-12 19:16:51 -06:00
cliffmccarthy
0a2ade038c refactor: Reorder deploy_chatmail()
- The previous commits that added Deployer classes mostly kept
  deployment operations in the same order that they were in before.
  To organize the process into separate stages for install, configure,
  and activate, we need to reorder the method calls.  This is the
  commit that does that, and thus this is the commit that has the
  largest effect on the order of operations.
- The calls for the deployer objects are all reordered here so that
  the methods are called in the same sequence for each stage.  This
  will allow us to collect the calls into loops in the next commit.
  This commit provides a way to see a diff showing exactly how the
  sequence changed.
- The sequence of deployers was largely based on preserving the order
  of the "activate" stage, as this seems like the place order might be
  the most likely to matter.  Installation of packages and
  configuration of files should generally be able to run in any order.
  (ChatmailDeployer handles updating the apt data, and therefore needs
  to be first, however.)
2025-11-12 19:16:51 -06:00
cliffmccarthy
67c5cf3204 refactor: Move curl installation from IrohDeployer to ChatmailDeployer
- The 'curl' program is used in TurnDeployer and IrohDeployer, so it
  makes more sense to install it at the beginning in ChatmailDeployer,
  rather than have each thing that uses it install it separately.
2025-11-12 19:16:51 -06:00
cliffmccarthy
e70c023541 refactor: Add TurnDeployer
- This splits the existing deploy_turn_server() routine into methods
  for the install, configure, and activate stages.
2025-11-12 19:16:51 -06:00
cliffmccarthy
7b75944f6b refactor: Add WebsiteDeployer
- This adds a step to create /var/www in the install stage, because
  the directory needs to exist for the rsync in the configure stage to
  work.
2025-11-12 19:16:51 -06:00
cliffmccarthy
3b44b61586 refactor: Add EchobotDeployer
- This class is a special case because it has a dependency on the
  Postfix and Dovecot deployers.  When deciding whether to restart the
  echobot service, it needs to know whether the Postfix and Dovecot
  deployers restarted their services.  To support this dependency, the
  PostfixDeployer and DovecotDeployer objects are passed to the
  EchobotDeployer object, so it can check their was_restarted
  attributes.
2025-11-12 19:16:51 -06:00
cliffmccarthy
533f0afde0 refactor: Add FcgiwrapDeployer 2025-11-12 19:16:51 -06:00
cliffmccarthy
3e4a602a5d refactor: Add ChatmailDeployer
- This moves the installation of cron earlier in the deployment sequence.
2025-11-12 19:16:51 -06:00
cliffmccarthy
e1d5d3e609 refactor: Add ChatmailVenvDeployer 2025-11-12 19:16:51 -06:00
cliffmccarthy
4dd041d799 refactor: Split _install_remote_venv_with_chatmaild into stages
- Split _install_remote_venv_with_chatmaild() into three routines, to
  handle the install, configure, and activate stages.
- This moves the upload of chatmail.ini later in the deployment
  process, because it is a configuration file specific to the
  instance, not software installation that would be uniform across all
  deployments.
2025-11-12 19:16:51 -06:00
cliffmccarthy
54c6bf6351 refactor: Add RspamdDeployer
- This replaces the existing _remove_rspamd() routine with a method
  for the install stage.
2025-11-12 19:16:51 -06:00
cliffmccarthy
f904c4e400 refactor: Add MtastsDeployer
- This splits the existing _uninstall_mta_sts_daemon() routine into
  methods for the configure and activate stages.
2025-11-12 19:16:51 -06:00
cliffmccarthy
a1972acf8f refactor: Add MtailDeployer
- This splits the existing deploy_mtail() routine into methods for the
  install, configure, and activate stages.
2025-11-12 19:16:51 -06:00
cliffmccarthy
afc1be2671 refactor: Add AcmetoolDeployer
- This splits the existing deploy_acmetool() routine into methods for
  the install, configure, and activate stages.
2025-11-12 19:16:51 -06:00
cliffmccarthy
6afd31fb17 refactor: Add JournaldDeployer 2025-11-12 19:16:51 -06:00
cliffmccarthy
93d9c0eb40 refactor: Add IrohDeployer
- This splits the existing deploy_iroh_relay() routine into methods
  for the install, configure, and activate stages.
2025-11-12 19:16:51 -06:00
cliffmccarthy
e3718eb4f8 refactor: Add UnboundDeployer 2025-11-12 19:16:51 -06:00
cliffmccarthy
b43059764b refactor: Add OpendkimDeployer
- Note that this moves the installation of the opendkim package
  earlier in the deployment sequence.  Previously, it was installed
  during the _configure_opendkim() routine.
2025-11-12 19:16:51 -06:00
cliffmccarthy
95edf42069 refactor: Add NginxDeployer
- Use policy-rc.d during nginx install.  This is needed to keep nginx
  from starting up and interfering with acmetool.  For more information see:
    - https://serverfault.com/questions/861583/how-to-stop-nginx-from-being-automatically-started-on-install
    - https://major.io/p/install-debian-packages-without-starting-daemons/
    - https://people.debian.org/~hmh/invokerc.d-policyrc.d-specification.txt
2025-11-12 19:16:51 -06:00
cliffmccarthy
b966c37740 refactor: Add PostfixDeployer
- Removed now-unused 'debug' variable from deploy_chatmail().
2025-11-12 19:16:51 -06:00
cliffmccarthy
1d1522880e refactor: Add DovecotDeployer 2025-11-12 19:16:51 -06:00
cliffmccarthy
2aeea0d95f refactor: Add Deployer base class
- Added a Deployer class that defines the base for objects that will
  handle installation of individual components, with install,
  configure, and activate stages.  Subclasses will override the
  implementation methods of those stages as needed, while the base
  class handles all the logic of deciding which stages to execute.
- The CMDEPLOY_STAGES environment variable is used to determine what
  stages to run.  If this is not defined, all stages run as usual.
- Added import of Deployer to cmdeploy/__init__.py.  This is not yet
  used, but the next series of commits will use it.
- In deploy_chatmail(), define an empty list of deployers, and call
  the create_groups() and create_users() methods for the items in the
  list.  This list will get filled with Deployer objects in the next
  series of commits.
2025-11-12 19:16:51 -06:00
cliffmccarthy
8bb0c20276 refactor: Move addition of 9.9.9.9 resolver earlier
- Moved the "Add 9.9.9.9 to resolv.conf" step earlier, before the
  creation of users or updates to any config files.  This should not
  affect any of those operations.  Moving this step earlier makes it
  easier to accommodate the restructuring of the deployment process
  into separate components with separate stages for install,
  configure, and activate.
2025-11-12 19:16:51 -06:00
cliffmccarthy
e6c97786dc refactor: Move all imports to top of cmdeploy/__init__.py 2025-11-12 19:16:51 -06:00
13 changed files with 110 additions and 195 deletions

View File

@@ -11,6 +11,7 @@ from io import StringIO
from pathlib import Path
from chatmaild.config import Config, read_config
from cmdeploy.cmdeploy import Out
from pyinfra import facts, host, logger
from pyinfra.api import FactBase
from pyinfra.facts.files import File, Sha256File
@@ -18,10 +19,8 @@ from pyinfra.facts.server import Sysctl
from pyinfra.facts.systemd import SystemdEnabled
from pyinfra.operations import apt, files, pip, server, systemd
from cmdeploy.cmdeploy import Out
from .acmetool import AcmetoolDeployer
from .basedeploy import Deployer, Deployment
from .deployer import Deployer, Deployment
from .www import build_webpages, find_merge_conflict, get_paths
@@ -40,10 +39,6 @@ class Port(FactBase):
return output[0]
def get_resource(arg, pkg=__package__):
return importlib.resources.files(pkg).joinpath(arg)
def _build_chatmaild(dist_dir) -> None:
dist_dir = Path(dist_dir).resolve()
if dist_dir.exists():
@@ -123,7 +118,7 @@ def _configure_remote_venv_with_chatmaild(config) -> None:
)
files.template(
src=get_resource("metrics.cron.j2"),
src=importlib.resources.files(__package__).joinpath("metrics.cron.j2"),
dest="/etc/cron.d/chatmail-metrics",
user="root",
group="root",
@@ -153,7 +148,7 @@ def _configure_remote_units(mail_domain, units) -> None:
basename = fn if "." in fn else f"{fn}.service"
source_path = get_resource(f"service/{basename}.f")
source_path = importlib.resources.files(__package__).joinpath("service", f"{basename}.f")
content = source_path.read_text().format(**params).encode()
files.put(
@@ -184,12 +179,13 @@ def _activate_remote_units(units) -> None:
)
def _configure_opendkim(domain: str, dkim_selector: str = "dkim") -> bool:
"""Configures OpenDKIM"""
need_restart = False
main_config = files.template(
src=get_resource("opendkim/opendkim.conf"),
src=importlib.resources.files(__package__).joinpath("opendkim/opendkim.conf"),
dest="/etc/opendkim.conf",
user="root",
group="root",
@@ -199,7 +195,7 @@ def _configure_opendkim(domain: str, dkim_selector: str = "dkim") -> bool:
need_restart |= main_config.changed
screen_script = files.put(
src=get_resource("opendkim/screen.lua"),
src=importlib.resources.files(__package__).joinpath("opendkim/screen.lua"),
dest="/etc/opendkim/screen.lua",
user="root",
group="root",
@@ -208,7 +204,7 @@ def _configure_opendkim(domain: str, dkim_selector: str = "dkim") -> bool:
need_restart |= screen_script.changed
final_script = files.put(
src=get_resource("opendkim/final.lua"),
src=importlib.resources.files(__package__).joinpath("opendkim/final.lua"),
dest="/etc/opendkim/final.lua",
user="root",
group="root",
@@ -226,7 +222,7 @@ def _configure_opendkim(domain: str, dkim_selector: str = "dkim") -> bool:
)
keytable = files.template(
src=get_resource("opendkim/KeyTable"),
src=importlib.resources.files(__package__).joinpath("opendkim/KeyTable"),
dest="/etc/dkimkeys/KeyTable",
user="opendkim",
group="opendkim",
@@ -236,7 +232,7 @@ def _configure_opendkim(domain: str, dkim_selector: str = "dkim") -> bool:
need_restart |= keytable.changed
signing_table = files.template(
src=get_resource("opendkim/SigningTable"),
src=importlib.resources.files(__package__).joinpath("opendkim/SigningTable"),
dest="/etc/dkimkeys/SigningTable",
user="opendkim",
group="opendkim",
@@ -265,7 +261,7 @@ def _configure_opendkim(domain: str, dkim_selector: str = "dkim") -> bool:
service_file = files.put(
name="Configure opendkim to restart once a day",
src=get_resource("opendkim/systemd.conf"),
src=importlib.resources.files(__package__).joinpath("opendkim/systemd.conf"),
dest="/etc/systemd/system/opendkim.service.d/10-prevent-memory-leak.conf",
)
need_restart |= service_file.changed
@@ -316,7 +312,7 @@ class UnboundDeployer(Deployer):
# https://people.debian.org/~hmh/invokerc.d-policyrc.d-specification.txt
#
files.put(
src=get_resource("policy-rc.d"),
src=importlib.resources.files(__package__).joinpath("policy-rc.d"),
dest="/usr/sbin/policy-rc.d",
user="root",
group="root",
@@ -376,7 +372,7 @@ def _configure_postfix(config: Config, debug: bool = False) -> bool:
need_restart = False
main_config = files.template(
src=get_resource("postfix/main.cf.j2"),
src=importlib.resources.files(__package__).joinpath("postfix/main.cf.j2"),
dest="/etc/postfix/main.cf",
user="root",
group="root",
@@ -387,7 +383,7 @@ def _configure_postfix(config: Config, debug: bool = False) -> bool:
need_restart |= main_config.changed
master_config = files.template(
src=get_resource("postfix/master.cf.j2"),
src=importlib.resources.files(__package__).joinpath("postfix/master.cf.j2"),
dest="/etc/postfix/master.cf",
user="root",
group="root",
@@ -398,7 +394,9 @@ def _configure_postfix(config: Config, debug: bool = False) -> bool:
need_restart |= master_config.changed
header_cleanup = files.put(
src=get_resource("postfix/submission_header_cleanup"),
src=importlib.resources.files(__package__).joinpath(
"postfix/submission_header_cleanup"
),
dest="/etc/postfix/submission_header_cleanup",
user="root",
group="root",
@@ -408,7 +406,7 @@ def _configure_postfix(config: Config, debug: bool = False) -> bool:
# Login map that 1:1 maps email address to login.
login_map = files.put(
src=get_resource("postfix/login_map"),
src=importlib.resources.files(__package__).joinpath("postfix/login_map"),
dest="/etc/postfix/login_map",
user="root",
group="root",
@@ -439,9 +437,7 @@ class PostfixDeployer(Deployer):
restart = False if self.disable_mail else self.need_restart
systemd.service(
name="disable postfix for now"
if self.disable_mail
else "Start and enable Postfix",
name="disable postfix for now" if self.disable_mail else "Start and enable Postfix",
service="postfix.service",
running=False if self.disable_mail else True,
enabled=False if self.disable_mail else True,
@@ -489,7 +485,7 @@ def _configure_dovecot(config: Config, debug: bool = False) -> bool:
need_restart = False
main_config = files.template(
src=get_resource("dovecot/dovecot.conf.j2"),
src=importlib.resources.files(__package__).joinpath("dovecot/dovecot.conf.j2"),
dest="/etc/dovecot/dovecot.conf",
user="root",
group="root",
@@ -500,7 +496,7 @@ def _configure_dovecot(config: Config, debug: bool = False) -> bool:
)
need_restart |= main_config.changed
auth_config = files.put(
src=get_resource("dovecot/auth.conf"),
src=importlib.resources.files(__package__).joinpath("dovecot/auth.conf"),
dest="/etc/dovecot/auth.conf",
user="root",
group="root",
@@ -508,7 +504,9 @@ def _configure_dovecot(config: Config, debug: bool = False) -> bool:
)
need_restart |= auth_config.changed
lua_push_notification_script = files.put(
src=get_resource("dovecot/push_notification.lua"),
src=importlib.resources.files(__package__).joinpath(
"dovecot/push_notification.lua"
),
dest="/etc/dovecot/push_notification.lua",
user="root",
group="root",
@@ -516,6 +514,13 @@ def _configure_dovecot(config: Config, debug: bool = False) -> bool:
)
need_restart |= lua_push_notification_script.changed
# remove historic expunge script
# which is now implemented through a systemd chatmail-expire service/timer
files.file(
path="/etc/cron.d/expunge",
present=False,
)
# as per https://doc.dovecot.org/configuration_manual/os/
# it is recommended to set the following inotify limits
for name in ("max_user_instances", "max_user_watches"):
@@ -564,9 +569,7 @@ class DovecotDeployer(Deployer):
restart = False if self.disable_mail else self.need_restart
systemd.service(
name="disable dovecot for now"
if self.disable_mail
else "Start and enable Dovecot",
name="disable dovecot for now" if self.disable_mail else "Start and enable Dovecot",
service="dovecot.service",
running=False if self.disable_mail else True,
enabled=False if self.disable_mail else True,
@@ -580,7 +583,7 @@ def _configure_nginx(config: Config, debug: bool = False) -> bool:
need_restart = False
main_config = files.template(
src=get_resource("nginx/nginx.conf.j2"),
src=importlib.resources.files(__package__).joinpath("nginx/nginx.conf.j2"),
dest="/etc/nginx/nginx.conf",
user="root",
group="root",
@@ -591,7 +594,7 @@ def _configure_nginx(config: Config, debug: bool = False) -> bool:
need_restart |= main_config.changed
autoconfig = files.template(
src=get_resource("nginx/autoconfig.xml.j2"),
src=importlib.resources.files(__package__).joinpath("nginx/autoconfig.xml.j2"),
dest="/var/www/html/.well-known/autoconfig/mail/config-v1.1.xml",
user="root",
group="root",
@@ -601,7 +604,7 @@ def _configure_nginx(config: Config, debug: bool = False) -> bool:
need_restart |= autoconfig.changed
mta_sts_config = files.template(
src=get_resource("nginx/mta-sts.txt.j2"),
src=importlib.resources.files(__package__).joinpath("nginx/mta-sts.txt.j2"),
dest="/var/www/html/.well-known/mta-sts.txt",
user="root",
group="root",
@@ -622,7 +625,7 @@ def _configure_nginx(config: Config, debug: bool = False) -> bool:
files.put(
name="Upload cgi newemail.py script",
src=get_resource("newemail.py", pkg="chatmaild").open("rb"),
src=importlib.resources.files("chatmaild").joinpath("newemail.py").open("rb"),
dest=f"{cgi_dir}/newemail.py",
user="root",
group="root",
@@ -657,7 +660,7 @@ class NginxDeployer(Deployer):
# https://people.debian.org/~hmh/invokerc.d-policyrc.d-specification.txt
#
files.put(
src=get_resource("policy-rc.d"),
src=importlib.resources.files(__package__).joinpath("policy-rc.d"),
dest="/usr/sbin/policy-rc.d",
user="root",
group="root",
@@ -705,9 +708,7 @@ class WebsiteDeployer(Deployer):
if not www_path.is_dir():
logger.warning("Building web pages is disabled in chatmail.ini, skipping")
elif (path := find_merge_conflict(src_dir)) is not None:
logger.warning(
f"Merge conflict found in {path}, skipping website deployment. Fix merge conflict if you want to upload your web page."
)
logger.warning(f"Merge conflict found in {path}, skipping website deployment. Fix merge conflict if you want to upload your web page.")
else:
# if www_folder is a hugo page, build it
if build_dir:
@@ -718,34 +719,10 @@ class WebsiteDeployer(Deployer):
)
class LegacyRemoveDeployer(Deployer):
class RspamdDeployer(Deployer):
def install(self):
apt.packages(name="Remove rspamd", packages="rspamd", present=False)
# remove historic expunge script
# which is now implemented through a systemd timer (chatmail-expire)
files.file(
path="/etc/cron.d/expunge",
present=False,
)
# Remove OBS repository key that is no longer used.
files.file("/etc/apt/keyrings/obs-home-deltachat.gpg", present=False)
files.line(
name="Remove DeltaChat OBS home repository from sources.list",
path="/etc/apt/sources.list",
line="deb [signed-by=/etc/apt/keyrings/obs-home-deltachat.gpg] https://download.opensuse.org/repositories/home:/deltachat/Debian_12/ ./",
escape_regex_characters=True,
present=False,
)
# prior relay versions used filelogging
files.directory(
name="Ensure old logs on disk are deleted",
path="/var/log/journal/",
present=False,
)
def check_config(config):
mail_domain = config.mail_domain
@@ -801,7 +778,7 @@ class MtailDeployer(Deployer):
self.mtail_address = mtail_address
def install(self):
# Uninstall mtail package to install a static binary.
# Uninstall mtail package, we are going to install a static binary.
apt.packages(name="Uninstall mtail", packages=["mtail"], present=False)
(url, sha256sum) = {
@@ -827,7 +804,9 @@ class MtailDeployer(Deployer):
# Using our own systemd unit instead of `/usr/lib/systemd/system/mtail.service`.
# This allows to read from journalctl instead of log files.
files.template(
src=get_resource("mtail/mtail.service.j2"),
src=importlib.resources.files(__package__).joinpath(
"mtail/mtail.service.j2"
),
dest="/etc/systemd/system/mtail.service",
user="root",
group="root",
@@ -838,7 +817,9 @@ class MtailDeployer(Deployer):
mtail_conf = files.put(
name="Mtail configuration",
src=get_resource("mtail/delivered_mail.mtail"),
src=importlib.resources.files(__package__).joinpath(
"mtail/delivered_mail.mtail"
),
dest="/etc/mtail/delivered_mail.mtail",
user="root",
group="root",
@@ -888,7 +869,7 @@ class IrohDeployer(Deployer):
def configure(self):
systemd_unit = files.put(
name="Upload iroh-relay systemd unit",
src=get_resource("iroh-relay.service"),
src=importlib.resources.files(__package__).joinpath("iroh-relay.service"),
dest="/etc/systemd/system/iroh-relay.service",
user="root",
group="root",
@@ -898,7 +879,7 @@ class IrohDeployer(Deployer):
iroh_config = files.put(
name="Upload iroh-relay config",
src=get_resource("iroh-relay.toml"),
src=importlib.resources.files(__package__).joinpath("iroh-relay.toml"),
dest="/etc/iroh-relay.toml",
user="root",
group="root",
@@ -921,7 +902,7 @@ class JournaldDeployer(Deployer):
def configure(self):
journald_conf = files.put(
name="Configure journald",
src=get_resource("journald.conf"),
src=importlib.resources.files(__package__).joinpath("journald.conf"),
dest="/etc/systemd/journald.conf",
user="root",
group="root",
@@ -1000,6 +981,17 @@ class ChatmailDeployer(Deployer):
self.mail_domain = mail_domain
def install(self):
# Remove OBS repository key that is no longer used.
files.file("/etc/apt/keyrings/obs-home-deltachat.gpg", present=False)
files.line(
name="Remove DeltaChat OBS home repository from sources.list",
path="/etc/apt/sources.list",
line="deb [signed-by=/etc/apt/keyrings/obs-home-deltachat.gpg] https://download.opensuse.org/repositories/home:/deltachat/Debian_12/ ./",
escape_regex_characters=True,
present=False,
)
apt.update(name="apt update", cache_time=24 * 3600)
apt.upgrade(name="upgrade apt packages", auto_remove=True)
@@ -1109,16 +1101,17 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
all_deployers = [
ChatmailDeployer(mail_domain),
LegacyRemoveDeployer(),
JournaldDeployer(),
UnboundDeployer(),
TurnDeployer(mail_domain),
IrohDeployer(config.enable_iroh_relay),
AcmetoolDeployer(config.acme_email, tls_domains),
WebsiteDeployer(config),
ChatmailVenvDeployer(config),
MtastsDeployer(),
OpendkimDeployer(mail_domain),
# Dovecot should be started before Postfix
# because it creates authentication socket
# required by Postfix.
@@ -1126,6 +1119,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
PostfixDeployer(config, disable_mail),
FcgiwrapDeployer(),
NginxDeployer(config),
RspamdDeployer(),
EchobotDeployer(mail_domain),
MtailDeployer(config.mtail_address),
GithashDeployer(),
@@ -1133,3 +1127,8 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
Deployment().perform_stages(all_deployers)
files.directory(
name="Ensure old logs on disk are deleted",
path="/var/log/journal/",
present=False,
)

View File

@@ -2,7 +2,7 @@ import importlib.resources
from pyinfra.operations import apt, files, server, systemd
from ..basedeploy import Deployer
from ..deployer import Deployer
class AcmetoolDeployer(Deployer):
@@ -27,9 +27,7 @@ class AcmetoolDeployer(Deployer):
files.put(
name="Install acmetool hook.",
src=importlib.resources.files(__package__)
.joinpath("acmetool.hook")
.open("rb"),
src=importlib.resources.files(__package__).joinpath("acmetool.hook").open("rb"),
dest="/etc/acme/hooks/nginx",
user="root",
group="root",
@@ -43,9 +41,7 @@ class AcmetoolDeployer(Deployer):
def configure(self):
files.template(
src=importlib.resources.files(__package__).joinpath(
"response-file.yaml.j2"
),
src=importlib.resources.files(__package__).joinpath("response-file.yaml.j2"),
dest="/var/lib/acme/conf/responses",
user="root",
group="root",
@@ -84,9 +80,7 @@ class AcmetoolDeployer(Deployer):
self.need_restart_reconcile_service = reconcile_service_file.changed
reconcile_timer_file = files.put(
src=importlib.resources.files(__package__).joinpath(
"acmetool-reconcile.timer"
),
src=importlib.resources.files(__package__).joinpath("acmetool-reconcile.timer"),
dest="/etc/systemd/system/acmetool-reconcile.timer",
user="root",
group="root",

View File

@@ -19,7 +19,7 @@ from packaging import version
from termcolor import colored
from . import dns, remote
from .sshexec import LocalExec, SSHExec
from .sshexec import SSHExec, LocalExec
#
# cmdeploy sub commands and options
@@ -95,7 +95,7 @@ def run_cmd(args, out):
env["CHATMAIL_INI"] = args.inipath
env["CHATMAIL_DISABLE_MAIL"] = "True" if args.disable_mail else ""
env["CHATMAIL_REQUIRE_IROH"] = "True" if require_iroh else ""
deploy_path = importlib.resources.files(__package__).joinpath("run.py").resolve()
deploy_path = importlib.resources.files(__package__).joinpath("deploy.py").resolve()
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
cmd = f"{pyinf} --ssh-user root {ssh_host} {deploy_path} -y"
@@ -238,12 +238,7 @@ def fmt_cmd_options(parser):
def fmt_cmd(args, out):
"""Run formattting fixes on all chatmail source code."""
chatmaild_dir = importlib.resources.files("chatmaild").resolve()
cmdeploy_dir = chatmaild_dir.joinpath(
"..", "..", "..", "cmdeploy", "src", "cmdeploy"
).resolve()
sources = [str(chatmaild_dir), str(cmdeploy_dir)]
sources = [str(importlib.resources.files(x)) for x in ("chatmaild", "cmdeploy")]
format_args = [shutil.which("ruff"), "format"]
check_args = [shutil.which("ruff"), "check"]
@@ -314,7 +309,7 @@ def add_ssh_host_option(parser):
"--ssh-host",
dest="ssh_host",
help="Run commands on 'localhost', via '@docker', or on a specific SSH host "
"instead of chatmail.ini's mail_domain.",
"instead of chatmail.ini's mail_domain.",
)

View File

@@ -3,9 +3,7 @@ import os
import pyinfra
# pyinfra runs this module as a python file and not as a module so
# import paths must be absolute
from cmdeploy.deployers import deploy_chatmail
from cmdeploy import deploy_chatmail
def main():

View File

@@ -45,8 +45,7 @@ def check_full_zone(sshexec, remote_data, out, zonefile) -> int:
and return (exitcode, remote_data) tuple."""
required_diff, recommended_diff = sshexec.logged(
remote.rdns.check_zonefile,
kwargs=dict(zonefile=zonefile, verbose=False),
remote.rdns.check_zonefile, kwargs=dict(zonefile=zonefile, verbose=False),
)
returncode = 0

View File

@@ -12,7 +12,7 @@ All functions of this module
import re
from .rshell import CalledProcessError, log_progress, shell
from .rshell import CalledProcessError, shell, log_progress
def perform_initial_checks(mail_domain, pre_command=""):
@@ -26,9 +26,7 @@ def perform_initial_checks(mail_domain, pre_command=""):
WWW = query_dns("CNAME", f"www.{mail_domain}")
res = dict(mail_domain=mail_domain, A=A, AAAA=AAAA, MTA_STS=MTA_STS, WWW=WWW)
res["acme_account_url"] = shell(
pre_command + "acmetool account-url", fail_ok=True, print=log_progress
)
res["acme_account_url"] = shell(pre_command + "acmetool account-url", fail_ok=True, print=log_progress)
res["dkim_entry"], res["web_dkim_entry"] = get_dkim_entry(
mail_domain, pre_command, dkim_selector="opendkim"
)
@@ -47,7 +45,7 @@ def get_dkim_entry(mail_domain, pre_command, dkim_selector):
dkim_pubkey = shell(
f"{pre_command}openssl rsa -in /etc/dkimkeys/{dkim_selector}.private "
"-pubout 2>/dev/null | awk '/-/{next}{printf(\"%s\",$0)}'",
print=log_progress,
print=log_progress
)
except CalledProcessError:
return
@@ -64,9 +62,9 @@ def query_dns(typ, domain):
# Get autoritative nameserver from the SOA record.
soa_answers = [
x.split()
for x in shell(
f"dig -r -q {domain} -t SOA +noall +authority +answer", print=log_progress
).split("\n")
for x in shell(f"dig -r -q {domain} -t SOA +noall +authority +answer", print=log_progress).split(
"\n"
)
]
soa = [a for a in soa_answers if len(a) >= 3 and a[3] == "SOA"]
if not soa:
@@ -75,7 +73,7 @@ def query_dns(typ, domain):
# Query authoritative nameserver directly to bypass DNS cache.
res = shell(f"dig @{ns} -r -q {domain} -t {typ} +short", print=log_progress)
return next((line for line in res.split("\n") if not line.startswith(";")), "")
return next((line for line in res.split("\n") if not line.startswith(';')), '')
def check_zonefile(zonefile, verbose=True):

View File

@@ -1,4 +1,5 @@
import sys
from subprocess import DEVNULL, CalledProcessError, check_output

View File

@@ -93,7 +93,7 @@ class LocalExec:
where = "locally"
if self.docker:
if call == remote.rdns.perform_initial_checks:
kwargs["pre_command"] = "docker exec chatmail "
kwargs['pre_command'] = "docker exec chatmail "
where = "in docker"
if self.verbose:
print(f"Running {where}: {call.__name__}(**{kwargs})")

View File

@@ -35,7 +35,7 @@ class TestSSHExecutor:
out, err = capsys.readouterr()
assert err.startswith("Collecting")
# XXX could not figure out how capturing can be made to work properly
# assert err.endswith("....\n")
#assert err.endswith("....\n")
assert err.count("\n") == 1
sshexec.verbose = True
@@ -45,7 +45,7 @@ class TestSSHExecutor:
out, err = capsys.readouterr()
lines = err.split("\n")
# XXX could not figure out how capturing can be made to work properly
# assert len(lines) > 4
#assert len(lines) > 4
assert remote.rdns.perform_initial_checks.__doc__ in lines[0]
def test_exception(self, sshexec, capsys):

View File

@@ -65,9 +65,7 @@ class TestPerformInitialChecks:
remote_data = remote.rdns.perform_initial_checks("some.domain")
assert remote_data["A"] == mockdns_expected["A"]["some.domain"]
assert remote_data["AAAA"] == mockdns_expected["AAAA"]["some.domain"]
assert (
remote_data["MTA_STS"] == mockdns_expected["CNAME"]["mta-sts.some.domain"]
)
assert remote_data["MTA_STS"] == mockdns_expected["CNAME"]["mta-sts.some.domain"]
assert remote_data["WWW"] == mockdns_expected["CNAME"]["www.some.domain"]
@pytest.mark.parametrize("drop", ["A", "AAAA"])

View File

@@ -1,10 +1,10 @@
import hashlib
import importlib.resources
import re
import time
import traceback
import webbrowser
from pathlib import Path
import re
import markdown
from chatmaild.config import read_config
@@ -12,9 +12,8 @@ from jinja2 import Template
from .genqr import gen_qr_png_data
_MERGE_CONFLICT_RE = re.compile(
r"^<<<<<<<.+^=======.+^>>>>>>>", re.DOTALL | re.MULTILINE
)
_MERGE_CONFLICT_RE = re.compile(r"^<<<<<<<.+^=======.+^>>>>>>>", re.DOTALL | re.MULTILINE)
def snapshot_dir_stats(somedir):

View File

@@ -126,13 +126,14 @@ web page. Edit them before deploying to make your chatmail relay
stand out.
Chatmail relay dependency diagram
---------------------------------
Component dependency diagram
--------------------------------------
.. mermaid::
:caption: This diagram shows relay components and dependencies/communication paths.
graph LR;
cmdeploy --- sshd;
letsencrypt --- |80|acmetool-redirector;
acmetool-redirector --- |443|nginx-right(["`nginx
(external)`"]);
@@ -146,100 +147,33 @@ Chatmail relay dependency diagram
nginx-internal --- autoconfig.xml;
certs-nginx[("`TLS certs
/var/lib/acme`")] --> nginx-internal;
systemd-timer --- chatmail-metrics;
systemd-timer --- acmetool;
systemd-timer --- chatmail-expire-daily;
systemd-timer --- chatmail-fsreport-daily;
cron --- chatmail-metrics;
cron --- acmetool;
chatmail-metrics --- website;
acmetool --> certs[("`TLS certs
/var/lib/acme`")];
nginx-external --- |993|dovecot;
postfix --- |SASL|dovecot;
autoconfig.xml --- postfix;
autoconfig.xml --- dovecot;
postfix --- |10080|filtermail-outgoing;
postfix --- |10081|filtermail-incoming;
filtermail-outgoing --- |10025 reinject|postfix;
filtermail-incoming --- |10026 reinject|postfix;
postfix --- echobot;
postfix --- |10080,10081|filtermail;
postfix --- users["`User data
home/vmail/mail`"];
postfix --- |doveauth.socket|doveauth;
dovecot --- |doveauth.socket|doveauth;
dovecot --- |message delivery|maildir["maildir
/home/vmail/.../user"];
dovecot --- |lastlogin.socket|lastlogin;
dovecot --- chatmail-metadata;
lastlogin --- maildir;
doveauth --- maildir;
chatmail-expire-daily --- maildir;
chatmail-fsreport-daily --- maildir;
dovecot --- users;
dovecot --- |metadata.socket|chatmail-metadata;
doveauth --- users;
chatmail-expire-daily --- users;
chatmail-fsreport-daily --- users;
chatmail-metadata --- iroh-relay;
chatmail-metadata --- |encrypted device token| notifications.delta.chat;
certs-nginx --> postfix;
certs-nginx --> dovecot;
style certs fill:#ff6;
style website fill:#ff6;
style maildir fill:#ff6;
style certs-nginx fill:#ff6;
style nginx-external fill:#f66;
style nginx-right fill:#f66;
style postfix fill:#f66;
style dovecot fill:#f66;
style notification-proxy fill:#f66;
style nginx-external fill:#fc9;
style nginx-right fill:#fc9;
Message between users on the same relay
---------------------------------------
.. mermaid::
:caption: This diagram shows the path a non-federated message takes.
graph LR;
sender --> |465|smtps/smtpd;
sender --> |587|submission/smtpd;
smtps/smtpd --> |10080|filtermail;
submission/smtpd --> |10080|filtermail;
filtermail --> |10025|smtpd_reinject;
smtpd_reinject --> cleanup;
cleanup --> qmgr;
qmgr --> smtpd_accepts_message;
qmgr --> |lmtp|dovecot;
dovecot --> recipient;
dovecot --> sender's_other_devices;
Message to an external address
------------------------------
.. mermaid::
:caption: This diagram shows the path a federated message takes.
graph LR;
sender --> |465|smtps/smtpd;
sender --> |587|submission/smtpd;
smtps/smtpd --> |10080|filtermail;
submission/smtpd --> |10080|filtermail;
filtermail --> |10025|smtpd_reinject;
smtpd_reinject --> opendkim;
opendkim --> smtpd_reinject;
smtpd_reinject --> cleanup;
cleanup --> qmgr;
qmgr --> external_smtp_server;
qmgr --> |lmtp|dovecot;
external_smtp_server --> recipient;
dovecot --> senders_other_devices;
Message from an external address
--------------------------------
.. mermaid::
:caption: This diagram shows the path a federated message takes.
graph LR;
external_smtp_server --> |25|smtpd;
smtps/smtpd --> |10081|filtermail-incoming;
filtermail-incoming --> |10026|smtpd_reinject_incoming;
smtpd_reinject_incoming --> opendkim;
opendkim --> smtpd_reinject_incoming;
smtpd_reinject_incoming --> cleanup;
cleanup --> qmgr;
qmgr --> |lmtp|dovecot;
dovecot --> recipient;
Operational details of a chatmail relay
----------------------------------------