Compare commits

...

7 Commits

11 changed files with 109 additions and 93 deletions

View File

@@ -71,26 +71,35 @@ jobs:
- name: run deploy-chatmail offline tests - name: run deploy-chatmail offline tests
run: pytest --pyargs cmdeploy run: pytest --pyargs cmdeploy
- run: | - name: setup dependencies
cmdeploy init staging-ipv4.testrun.org run: |
sed -i 's#disable_ipv6 = False#disable_ipv6 = True#' chatmail.ini ssh root@staging-ipv4.testrun.org apt update
sed -i 's/#\s*mtail_address/mtail_address/' chatmail.ini ssh root@staging-ipv4.testrun.org apt install -y git python3.11-venv python3-dev gcc
ssh root@staging-ipv4.testrun.org git clone https://github.com/chatmail/relay
ssh root@staging-ipv4.testrun.org "cd relay && git checkout " ${{ github.head_ref }}
ssh root@staging-ipv4.testrun.org "cd relay && scripts/initenv.sh"
- run: cmdeploy run --verbose --skip-dns-check - name: initialize config
run: |
ssh root@staging-ipv4.testrun.org "cd relay && scripts/cmdeploy init staging-ipv4.testrun.org"
ssh root@staging-ipv4.testrun.org "sed -i 's#disable_ipv6 = False#disable_ipv6 = True#' relay/chatmail.ini"
ssh root@staging-ipv4.testrun.org "sed -i 's/#\s*mtail_address/mtail_address/' relay/chatmail.ini"
- run: ssh root@staging-ipv4.testrun.org "cd relay && scripts/cmdeploy run --verbose --skip-dns-check"
- name: set DNS entries - name: set DNS entries
run: | run: |
ssh -o StrictHostKeyChecking=accept-new -v root@staging-ipv4.testrun.org chown opendkim:opendkim -R /etc/dkimkeys ssh root@staging-ipv4.testrun.org chown opendkim:opendkim -R /etc/dkimkeys
cmdeploy dns --zonefile staging-generated.zone ssh root@staging-ipv4.testrun.org "cd relay && scripts/cmdeploy dns --zonefile staging-generated.zone"
cat staging-generated.zone >> .github/workflows/staging-ipv4.testrun.org-default.zone ssh root@staging-ipv4.testrun.org cat relay/staging-generated.zone >> .github/workflows/staging-ipv4.testrun.org-default.zone
cat .github/workflows/staging-ipv4.testrun.org-default.zone cat .github/workflows/staging-ipv4.testrun.org-default.zone
scp .github/workflows/staging-ipv4.testrun.org-default.zone root@ns.testrun.org:/etc/nsd/staging-ipv4.testrun.org.zone scp .github/workflows/staging-ipv4.testrun.org-default.zone root@ns.testrun.org:/etc/nsd/staging-ipv4.testrun.org.zone
ssh root@ns.testrun.org nsd-checkzone staging-ipv4.testrun.org /etc/nsd/staging-ipv4.testrun.org.zone ssh root@ns.testrun.org nsd-checkzone staging-ipv4.testrun.org /etc/nsd/staging-ipv4.testrun.org.zone
ssh root@ns.testrun.org systemctl reload nsd ssh root@ns.testrun.org systemctl reload nsd
- name: cmdeploy test - name: cmdeploy test
run: CHATMAIL_DOMAIN2=ci-chatmail.testrun.org cmdeploy test --slow run: ssh root@staging-ipv4.testrun.org "cd relay && CHATMAIL_DOMAIN2=ci-chatmail.testrun.org scripts/cmdeploy test --slow"
- name: cmdeploy dns - name: cmdeploy dns
run: cmdeploy dns -v run: ssh root@staging-ipv4.testrun.org "cd relay && scripts/cmdeploy dns -v"

View File

@@ -76,6 +76,7 @@ jobs:
- run: | - run: |
cmdeploy init staging2.testrun.org cmdeploy init staging2.testrun.org
sed -i 's/^ssh_host/#ssh_host/' chatmail.ini
sed -i 's/#\s*mtail_address/mtail_address/' chatmail.ini sed -i 's/#\s*mtail_address/mtail_address/' chatmail.ini
- run: cmdeploy run --verbose --skip-dns-check - run: cmdeploy run --verbose --skip-dns-check

View File

@@ -9,30 +9,28 @@ from chatmaild.user import User
def read_config(inipath): def read_config(inipath):
assert Path(inipath).exists(), inipath assert Path(inipath).exists(), inipath
cfg = iniconfig.IniConfig(inipath) cfg = iniconfig.IniConfig(inipath)
params = cfg.sections["params"] return Config(inipath, params=cfg.sections["params"])
default_config_content = get_default_config_content(params["mail_domain"])
df_params = iniconfig.IniConfig("ini", data=default_config_content)["params"]
new_params = dict(df_params.items())
new_params.update(params)
return Config(inipath, params=new_params)
class Config: class Config:
def __init__(self, inipath, params): def __init__(self, inipath, params):
self._inipath = inipath self._inipath = inipath
self.mail_domain = params["mail_domain"] self.mail_domain = params["mail_domain"]
self.ssh_host = params.get("ssh_host", self.mail_domain)
self.max_user_send_per_minute = int(params.get("max_user_send_per_minute", 60)) self.max_user_send_per_minute = int(params.get("max_user_send_per_minute", 60))
self.max_user_send_burst_size = int(params.get("max_user_send_burst_size", 10)) self.max_user_send_burst_size = int(params.get("max_user_send_burst_size", 10))
self.max_mailbox_size = params["max_mailbox_size"] self.max_mailbox_size = params.get("max_mailbox_size", "500M")
self.max_message_size = int(params.get("max_message_size", "31457280")) self.max_message_size = int(params.get("max_message_size", 31457280))
self.delete_mails_after = params["delete_mails_after"] self.delete_mails_after = params.get("delete_mails_after", "20")
self.delete_large_after = params["delete_large_after"] self.delete_large_after = params.get("delete_large_after", "7")
self.delete_inactive_users_after = int(params["delete_inactive_users_after"]) self.delete_inactive_users_after = int(
self.username_min_length = int(params["username_min_length"]) params.get("delete_inactive_users_after", 100)
self.username_max_length = int(params["username_max_length"]) )
self.password_min_length = int(params["password_min_length"]) self.username_min_length = int(params.get("username_min_length", 9))
self.passthrough_senders = params["passthrough_senders"].split() self.username_max_length = int(params.get("username_max_length", 9))
self.passthrough_recipients = params["passthrough_recipients"].split() self.password_min_length = int(params.get("password_min_length", 9))
self.passthrough_senders = params.get("passthrough_senders", "").split()
self.passthrough_recipients = params.get("passthrough_recipients", "").split()
self.www_folder = params.get("www_folder", "") self.www_folder = params.get("www_folder", "")
self.filtermail_smtp_port = int(params.get("filtermail_smtp_port", "10080")) self.filtermail_smtp_port = int(params.get("filtermail_smtp_port", "10080"))
self.filtermail_smtp_port_incoming = int( self.filtermail_smtp_port_incoming = int(

View File

@@ -3,6 +3,9 @@
# mail domain (MUST be set to fully qualified chat mail domain) # mail domain (MUST be set to fully qualified chat mail domain)
mail_domain = {mail_domain} mail_domain = {mail_domain}
# Where to deploy the relay - if unspecified, mail_domain will be used.
ssh_host = localhost
# #
# If you only do private test deploys, you don't need to modify any settings below # If you only do private test deploys, you don't need to modify any settings below
# #

View File

@@ -88,7 +88,7 @@ def run_cmd_options(parser):
def run_cmd(args, out): def run_cmd(args, out):
"""Deploy chatmail services on the remote server.""" """Deploy chatmail services on the remote server."""
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain ssh_host = args.ssh_host if args.ssh_host else args.config.ssh_host
sshexec = get_sshexec(ssh_host) sshexec = get_sshexec(ssh_host)
require_iroh = args.config.enable_iroh_relay require_iroh = args.config.enable_iroh_relay
if not args.dns_check_disabled: if not args.dns_check_disabled:
@@ -108,7 +108,7 @@ def run_cmd(args, out):
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra" pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
cmd = f"{pyinf} --ssh-user root {ssh_host} {deploy_path} -y" cmd = f"{pyinf} --ssh-user root {ssh_host} {deploy_path} -y"
if ssh_host in ["localhost", "@docker"]: if ssh_host in ["localhost", "@local", "@docker"]:
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"):
@@ -149,7 +149,7 @@ def dns_cmd_options(parser):
def dns_cmd(args, out): def dns_cmd(args, out):
"""Check DNS entries and optionally generate dns zone file.""" """Check DNS entries and optionally generate dns zone file."""
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain ssh_host = args.ssh_host if args.ssh_host else args.config.ssh_host
sshexec = get_sshexec(ssh_host, verbose=args.verbose) sshexec = get_sshexec(ssh_host, verbose=args.verbose)
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain) remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
if not remote_data: if not remote_data:
@@ -183,7 +183,7 @@ def status_cmd_options(parser):
def status_cmd(args, out): def status_cmd(args, out):
"""Display status for online chatmail instance.""" """Display status for online chatmail instance."""
ssh_host = args.ssh_host if args.ssh_host else args.config.mail_domain ssh_host = args.ssh_host if args.ssh_host else args.config.ssh_host
sshexec = get_sshexec(ssh_host, verbose=args.verbose) sshexec = get_sshexec(ssh_host, verbose=args.verbose)
out.green(f"chatmail domain: {args.config.mail_domain}") out.green(f"chatmail domain: {args.config.mail_domain}")
@@ -203,6 +203,7 @@ def test_cmd_options(parser):
action="store_true", action="store_true",
help="also run slow tests", help="also run slow tests",
) )
add_ssh_host_option(parser)
def test_cmd(args, out): def test_cmd(args, out):
@@ -214,6 +215,9 @@ def test_cmd(args, out):
x = importlib.util.find_spec("deltachat") x = importlib.util.find_spec("deltachat")
if x is None: if x is None:
out.check_call(f"{sys.executable} -m pip install deltachat") out.check_call(f"{sys.executable} -m pip install deltachat")
env = os.environ.copy()
if args.ssh_host:
env["CHATMAIL_SSH"] = args.ssh_host
pytest_path = shutil.which("pytest") pytest_path = shutil.which("pytest")
pytest_args = [ pytest_args = [
@@ -227,7 +231,7 @@ def test_cmd(args, out):
] ]
if args.slow: if args.slow:
pytest_args.append("--slow") pytest_args.append("--slow")
ret = out.run_ret(pytest_args) ret = out.run_ret(pytest_args, env=env)
return ret return ret

View File

@@ -85,16 +85,31 @@ class SSHExec:
class LocalExec: class LocalExec:
FuncError = FuncError
def __init__(self, verbose=False, docker=False): def __init__(self, verbose=False, docker=False):
self.verbose = verbose self.verbose = verbose
self.docker = docker self.docker = docker
def __call__(self, call, kwargs=None, log_callback=None):
if kwargs is None:
kwargs = {}
return call(**kwargs)
def logged(self, call, kwargs: dict): def logged(self, call, kwargs: dict):
title = call.__doc__
if not title:
title = call.__name__
where = "locally" where = "locally"
if self.docker: if self.docker:
if call == remote.rdns.perform_initial_checks: if call == remote.rdns.perform_initial_checks:
kwargs["pre_command"] = "docker exec chatmail " kwargs["pre_command"] = "docker exec chatmail "
where = "in docker" where = "in docker"
if self.verbose: if self.verbose:
print(f"Running {where}: {call.__name__}(**{kwargs})") print_stderr(f"Running {where}: {title}(**{kwargs})")
return call(**kwargs) return self(call, kwargs, log_callback=print_stderr)
else:
print_stderr(title, end="")
res = self(call, kwargs, log_callback=remote.rshell.log_progress)
print_stderr()
return res

View File

@@ -7,13 +7,13 @@ import time
import pytest import pytest
from cmdeploy import remote from cmdeploy import remote
from cmdeploy.sshexec import SSHExec from cmdeploy.cmdeploy import get_sshexec
class TestSSHExecutor: class TestSSHExecutor:
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def sshexec(self, sshdomain): def sshexec(self, sshdomain):
return SSHExec(sshdomain) return get_sshexec(sshdomain)
def test_ls(self, sshexec): def test_ls(self, sshexec):
out = sshexec(call=remote.rdns.shell, kwargs=dict(command="ls")) out = sshexec(call=remote.rdns.shell, kwargs=dict(command="ls"))
@@ -27,6 +27,7 @@ class TestSSHExecutor:
assert res["A"] or res["AAAA"] assert res["A"] or res["AAAA"]
def test_logged(self, sshexec, maildomain, capsys): def test_logged(self, sshexec, maildomain, capsys):
sshexec.verbose = False
sshexec.logged( sshexec.logged(
remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain) remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=maildomain)
) )
@@ -52,6 +53,8 @@ class TestSSHExecutor:
remote.rdns.perform_initial_checks, remote.rdns.perform_initial_checks,
kwargs=dict(mail_domain=None), kwargs=dict(mail_domain=None),
) )
except AssertionError:
pass
except sshexec.FuncError as e: except sshexec.FuncError as e:
assert "rdns.py" in str(e) assert "rdns.py" in str(e)
assert "AssertionError" in str(e) assert "AssertionError" in str(e)

View File

@@ -7,7 +7,7 @@ import pytest
import requests import requests
from cmdeploy.remote import rshell from cmdeploy.remote import rshell
from cmdeploy.sshexec import SSHExec from cmdeploy.cmdeploy import get_sshexec
@pytest.fixture @pytest.fixture
@@ -90,7 +90,7 @@ class TestEndToEndDeltaChat:
lp.sec(f"filling remote inbox for {user}") lp.sec(f"filling remote inbox for {user}")
fn = f"7743102289.M843172P2484002.c20,S={quota},W=2398:2," fn = f"7743102289.M843172P2484002.c20,S={quota},W=2398:2,"
path = chatmail_config.mailboxes_dir.joinpath(user, "cur", fn) path = chatmail_config.mailboxes_dir.joinpath(user, "cur", fn)
sshexec = SSHExec(sshdomain) sshexec = get_sshexec(sshdomain)
sshexec(call=rshell.write_numbytes, kwargs=dict(path=str(path), num=120)) sshexec(call=rshell.write_numbytes, kwargs=dict(path=str(path), num=120))
res = sshexec(call=rshell.dovecot_recalc_quota, kwargs=dict(user=user)) res = sshexec(call=rshell.dovecot_recalc_quota, kwargs=dict(user=user))
assert res["percent"] >= 100 assert res["percent"] >= 100

View File

@@ -5,7 +5,11 @@ from cmdeploy.cmdeploy import main
def test_status_cmd(chatmail_config, capsys, request): def test_status_cmd(chatmail_config, capsys, request):
os.chdir(request.config.invocation_params.dir) os.chdir(request.config.invocation_params.dir)
assert main(["status"]) == 0 command = ["status"]
if os.getenv("CHATMAIL_SSH"):
command.append("--ssh-host")
command.append(os.getenv("CHATMAIL_SSH"))
assert main(command) == 0
status_out = capsys.readouterr() status_out = capsys.readouterr()
print(status_out.out) print(status_out.out)

View File

@@ -54,8 +54,8 @@ def maildomain(chatmail_config):
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def sshdomain(maildomain): def sshdomain(chatmail_config):
return os.environ.get("CHATMAIL_SSH", maildomain) return os.environ.get("CHATMAIL_SSH", chatmail_config.ssh_host)
@pytest.fixture @pytest.fixture
@@ -337,8 +337,14 @@ class Remote:
def iter_output(self, logcmd=""): def iter_output(self, logcmd=""):
getjournal = "journalctl -f" if not logcmd else logcmd getjournal = "journalctl -f" if not logcmd else logcmd
print(self.sshdomain)
match self.sshdomain:
case "@local": command = []
case "localhost": command = []
case _: command = ["ssh", f"root@{self.sshdomain}"]
[command.append(arg) for arg in getjournal.split()]
self.popen = subprocess.Popen( self.popen = subprocess.Popen(
["ssh", f"root@{self.sshdomain}", getjournal], command,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
) )
while 1: while 1:

View File

@@ -16,18 +16,11 @@ You will need the following:
- Control over a domain through a DNS provider of your choice. - Control over a domain through a DNS provider of your choice.
- A Debian 12 **deployment server** with reachable SMTP/SUBMISSIONS/IMAPS/HTTPS ports. - A Debian 12 server with reachable SMTP/SUBMISSIONS/IMAPS/HTTPS ports.
IPv6 is encouraged if available. Chatmail relay servers only require IPv6 is encouraged if available. Chatmail relay servers only require
1GB RAM, one CPU, and perhaps 10GB storage for a few thousand active 1GB RAM, one CPU, and perhaps 10GB storage for a few thousand active
chatmail addresses. chatmail addresses.
- A Linux or Unix **build machine** with key-based SSH access to the root
user of the deployment server.
You must add a passphrase-protected private key to your local ssh-agent because you
cant type in your passphrase during deployment.
(An ed25519 private key is required due to an `upstream bug in
paramiko <https://github.com/paramiko/paramiko/issues/2191>`_)
Setup with ``scripts/cmdeploy`` Setup with ``scripts/cmdeploy``
------------------------------------- -------------------------------------
@@ -35,7 +28,7 @@ Setup with ``scripts/cmdeploy``
We use ``chat.example.org`` as the chatmail domain in the following We use ``chat.example.org`` as the chatmail domain in the following
steps. Please substitute it with your own domain. steps. Please substitute it with your own domain.
1. Setup the initial DNS records for your deployment server. 1. Setup the initial DNS records for your relay.
The following is an example in the The following is an example in the
familiar BIND zone file format with a TTL of 1 hour (3600 seconds). familiar BIND zone file format with a TTL of 1 hour (3600 seconds).
Please substitute your domain and IP addresses. Please substitute your domain and IP addresses.
@@ -47,29 +40,24 @@ steps. Please substitute it with your own domain.
www.chat.example.org. 3600 IN CNAME chat.example.org. www.chat.example.org. 3600 IN CNAME chat.example.org.
mta-sts.chat.example.org. 3600 IN CNAME chat.example.org. mta-sts.chat.example.org. 3600 IN CNAME chat.example.org.
2. On your local PC, clone the repository and bootstrap the Python 2. Login to the server with SSH, clone the repository and bootstrap the Python
virtualenv. virtualenv.
:: ::
ssh root@chat.example.org
git clone https://github.com/chatmail/relay git clone https://github.com/chatmail/relay
cd relay cd relay
scripts/initenv.sh scripts/initenv.sh
3. On your local build machine (PC), create a chatmail configuration file 3. Then, create a chatmail configuration file
``chatmail.ini``: ``chatmail.ini``:
:: ::
scripts/cmdeploy init chat.example.org # <-- use your domain scripts/cmdeploy init chat.example.org # <-- use your domain
4. Verify that SSH root login to the deployment server server works: 4. Now run the deployment script to install the relay to the server:
::
ssh root@chat.example.org # <-- use your domain
5. From your local build machine, setup and configure the remote deployment server:
:: ::
@@ -80,27 +68,32 @@ steps. Please substitute it with your own domain.
configure at your DNS provider (it can take some time until they are configure at your DNS provider (it can take some time until they are
public). public).
Other helpful commands Next Steps
---------------------- ----------
To check the status of your deployment server running the chatmail service: Now you should display and check all recommended DNS records
to enable federation with other relays:
::
scripts/cmdeploy status
To display and check all recommended DNS records:
:: ::
scripts/cmdeploy dns scripts/cmdeploy dns
To test whether your chatmail service is working correctly: You should also test whether your chatmail service is working correctly:
:: ::
scripts/cmdeploy test scripts/cmdeploy test
Other Helpful Commands
----------------------
To check the status of your chatmail relay:
::
scripts/cmdeploy status
To measure the performance of your chatmail service: To measure the performance of your chatmail service:
:: ::
@@ -141,8 +134,9 @@ This starts a local live development cycle for chatmail web pages:
directory and generating HTML files and copying assets to the directory and generating HTML files and copying assets to the
``www/build`` directory. ``www/build`` directory.
- Starts a browser window automatically where you can “refresh” as - if you are running scripts/cmdeploy webdev on the relay itself,
needed. you need to configure a route in /etc/nginx/nginx.conf
to expose the build directory.
Custom web pages Custom web pages
---------------- ----------------
@@ -160,7 +154,7 @@ Disable automatic address creation
-------------------------------------------------------- --------------------------------------------------------
If you need to stop address creation, e.g. because some script is wildly If you need to stop address creation, e.g. because some script is wildly
creating addresses, login with ssh to the deployment machine and run: creating addresses, login with ssh to the relay and run:
:: ::
@@ -168,24 +162,3 @@ creating addresses, login with ssh to the deployment machine and run:
Chatmail address creation will be denied while this file is present. Chatmail address creation will be denied while this file is present.
Migrating to a new build machine
----------------------------------
To move or add a build machine,
clone the relay repository on the new build machine, and copy the ``chatmail.ini`` file from the old build machine.
Make sure ``rsync`` is installed, then initialize the environment:
::
./scripts/initenv.sh
Run safety checks before a new deployment:
::
./scripts/cmdeploy dns
./scripts/cmdeploy status
If you keep multiple build machines (ie laptop and desktop), keep ``chatmail.ini`` in sync between
them.