From 4af8380669903da775f6653ea9b101136fdab263 Mon Sep 17 00:00:00 2001 From: missytake Date: Wed, 8 Oct 2025 10:18:53 +0200 Subject: [PATCH 1/5] cmdeploy: prepare for being able to run commands in docker containers --- cmdeploy/src/cmdeploy/cmdeploy.py | 6 ++++-- cmdeploy/src/cmdeploy/dns.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cmdeploy/src/cmdeploy/cmdeploy.py b/cmdeploy/src/cmdeploy/cmdeploy.py index e1b0ded00..d19e375e7 100644 --- a/cmdeploy/src/cmdeploy/cmdeploy.py +++ b/cmdeploy/src/cmdeploy/cmdeploy.py @@ -99,7 +99,7 @@ def run_cmd(args, out): pyinf = "pyinfra --dry" if args.dry_run else "pyinfra" cmd = f"{pyinf} --ssh-user root {ssh_host} {deploy_path} -y" - if ssh_host == "localhost": + if ssh_host in ["localhost", "docker"]: cmd = f"{pyinf} @local {deploy_path} -y" if version.parse(pyinfra.__version__) < version.parse("3"): @@ -303,7 +303,7 @@ def add_ssh_host_option(parser): parser.add_argument( "--ssh-host", dest="ssh_host", - help="Run commands on 'localhost' or a specific SSH host " + help="Run commands on 'localhost', via '@docker', or on a specific SSH host " "instead of chatmail.ini's mail_domain.", ) @@ -366,6 +366,8 @@ def get_parser(): def get_sshexec(ssh_host: str, verbose=True): if ssh_host in ["localhost", "@local"]: return "localhost" + elif ssh_host == "docker": + return "docker" if verbose: print(f"[ssh] login to {ssh_host}") return SSHExec(ssh_host, verbose=verbose) diff --git a/cmdeploy/src/cmdeploy/dns.py b/cmdeploy/src/cmdeploy/dns.py index 6277d158a..0562181a6 100644 --- a/cmdeploy/src/cmdeploy/dns.py +++ b/cmdeploy/src/cmdeploy/dns.py @@ -9,6 +9,8 @@ def get_initial_remote_data(sshexec, mail_domain): if sshexec == "localhost": result = remote.rdns.perform_initial_checks(mail_domain) + elif sshexec == "docker": + result = remote.rdns.perform_initial_checks(mail_domain, pre_command="docker exec chatmail ") else: result = sshexec.logged( call=remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=mail_domain) @@ -48,7 +50,7 @@ def check_full_zone(sshexec, remote_data, out, zonefile) -> int: """Check existing DNS records, optionally write them to zone file and return (exitcode, remote_data) tuple.""" - if sshexec == "localhost": + if sshexec in ["localhost", "docker"]: required_diff, recommended_diff = remote.rdns.check_zonefile( zonefile=zonefile, verbose=False ) From ed0f3f758d33f9b2d04d421767baea4f0f805308 Mon Sep 17 00:00:00 2001 From: missytake Date: Tue, 14 Oct 2025 15:32:18 +0200 Subject: [PATCH 2/5] tests: first attempt to mock shell() call --- cmdeploy/src/cmdeploy/remote/rdns.py | 4 ++- cmdeploy/src/cmdeploy/remote/rshell.py | 3 ++- cmdeploy/src/cmdeploy/tests/test_cmdeploy.py | 26 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/cmdeploy/src/cmdeploy/remote/rdns.py b/cmdeploy/src/cmdeploy/remote/rdns.py index 7340a777c..ce66c46d3 100644 --- a/cmdeploy/src/cmdeploy/remote/rdns.py +++ b/cmdeploy/src/cmdeploy/remote/rdns.py @@ -11,13 +11,15 @@ """ import re - +from pprint import pprint from .rshell import CalledProcessError, shell, log_progress def perform_initial_checks(mail_domain, pre_command=""): """Collecting initial DNS settings.""" assert mail_domain + pprint("rdns.perform_initial_checks: " + shell.__module__) + if not shell("dig", fail_ok=True, print=log_progress): shell("apt-get update && apt-get install -y dnsutils", print=log_progress) A = query_dns("A", mail_domain) diff --git a/cmdeploy/src/cmdeploy/remote/rshell.py b/cmdeploy/src/cmdeploy/remote/rshell.py index f81668167..844758166 100644 --- a/cmdeploy/src/cmdeploy/remote/rshell.py +++ b/cmdeploy/src/cmdeploy/remote/rshell.py @@ -1,5 +1,5 @@ import sys - +from pprint import pprint from subprocess import DEVNULL, CalledProcessError, check_output @@ -9,6 +9,7 @@ def log_progress(data): def shell(command, fail_ok=False, print=print): + pprint("test_cmdeploy: " + shell.__module__) print(f"$ {command}") args = dict(shell=True) if fail_ok: diff --git a/cmdeploy/src/cmdeploy/tests/test_cmdeploy.py b/cmdeploy/src/cmdeploy/tests/test_cmdeploy.py index 6e87b4eac..6f9a2cc77 100644 --- a/cmdeploy/src/cmdeploy/tests/test_cmdeploy.py +++ b/cmdeploy/src/cmdeploy/tests/test_cmdeploy.py @@ -5,6 +5,8 @@ from cmdeploy.cmdeploy import get_parser, main from cmdeploy.www import get_paths +import cmdeploy.remote.rshell +import cmdeploy.dns @pytest.fixture(autouse=True) @@ -59,3 +61,27 @@ def test_www_folder(example_config, tmp_path): assert www_path == tmp_path assert src_dir == src_path assert build_dir == tmp_path.joinpath("build") + + +def test_dns_when_ssh_docker(monkeypatch): + commands = [] + + def shell(command, fail_ok=None, print=None): + assert command == False + commands.append(command) + + # mock shell function to add called commands to a global list + monkeypatch.setattr( + cmdeploy.remote.rshell, shell.__name__, shell + ) # still doesn't get called in get_initial_remote_data :( + print("test_cmdeploy: " + shell.__module__) + # run cmdeploy dns with --ssh-host + # @docker + cmdeploy.dns.get_initial_remote_data("@docker", "chatmail.example.org") + for cmd in commands: + print(cmd) + # localhost + # @local + # without --ssh-host + # check which commands were called + assert False From e40753c18de722afcc0f1fe255aea7d16af5dea5 Mon Sep 17 00:00:00 2001 From: missytake Date: Tue, 14 Oct 2025 19:31:31 +0200 Subject: [PATCH 3/5] Revert "tests: first attempt to mock shell() call" This reverts commit a0c632a7006a83c8b39cff86228296c32c5c5b9e. --- cmdeploy/src/cmdeploy/remote/rdns.py | 4 +-- cmdeploy/src/cmdeploy/remote/rshell.py | 3 +-- cmdeploy/src/cmdeploy/tests/test_cmdeploy.py | 26 -------------------- 3 files changed, 2 insertions(+), 31 deletions(-) diff --git a/cmdeploy/src/cmdeploy/remote/rdns.py b/cmdeploy/src/cmdeploy/remote/rdns.py index ce66c46d3..7340a777c 100644 --- a/cmdeploy/src/cmdeploy/remote/rdns.py +++ b/cmdeploy/src/cmdeploy/remote/rdns.py @@ -11,15 +11,13 @@ """ import re -from pprint import pprint + from .rshell import CalledProcessError, shell, log_progress def perform_initial_checks(mail_domain, pre_command=""): """Collecting initial DNS settings.""" assert mail_domain - pprint("rdns.perform_initial_checks: " + shell.__module__) - if not shell("dig", fail_ok=True, print=log_progress): shell("apt-get update && apt-get install -y dnsutils", print=log_progress) A = query_dns("A", mail_domain) diff --git a/cmdeploy/src/cmdeploy/remote/rshell.py b/cmdeploy/src/cmdeploy/remote/rshell.py index 844758166..f81668167 100644 --- a/cmdeploy/src/cmdeploy/remote/rshell.py +++ b/cmdeploy/src/cmdeploy/remote/rshell.py @@ -1,5 +1,5 @@ import sys -from pprint import pprint + from subprocess import DEVNULL, CalledProcessError, check_output @@ -9,7 +9,6 @@ def log_progress(data): def shell(command, fail_ok=False, print=print): - pprint("test_cmdeploy: " + shell.__module__) print(f"$ {command}") args = dict(shell=True) if fail_ok: diff --git a/cmdeploy/src/cmdeploy/tests/test_cmdeploy.py b/cmdeploy/src/cmdeploy/tests/test_cmdeploy.py index 6f9a2cc77..6e87b4eac 100644 --- a/cmdeploy/src/cmdeploy/tests/test_cmdeploy.py +++ b/cmdeploy/src/cmdeploy/tests/test_cmdeploy.py @@ -5,8 +5,6 @@ from cmdeploy.cmdeploy import get_parser, main from cmdeploy.www import get_paths -import cmdeploy.remote.rshell -import cmdeploy.dns @pytest.fixture(autouse=True) @@ -61,27 +59,3 @@ def test_www_folder(example_config, tmp_path): assert www_path == tmp_path assert src_dir == src_path assert build_dir == tmp_path.joinpath("build") - - -def test_dns_when_ssh_docker(monkeypatch): - commands = [] - - def shell(command, fail_ok=None, print=None): - assert command == False - commands.append(command) - - # mock shell function to add called commands to a global list - monkeypatch.setattr( - cmdeploy.remote.rshell, shell.__name__, shell - ) # still doesn't get called in get_initial_remote_data :( - print("test_cmdeploy: " + shell.__module__) - # run cmdeploy dns with --ssh-host - # @docker - cmdeploy.dns.get_initial_remote_data("@docker", "chatmail.example.org") - for cmd in commands: - print(cmd) - # localhost - # @local - # without --ssh-host - # check which commands were called - assert False From ebbb83724192eba97b9b8c44f26377a2217516d7 Mon Sep 17 00:00:00 2001 From: missytake Date: Tue, 14 Oct 2025 20:38:59 +0200 Subject: [PATCH 4/5] cmdeploy: introduce LocalExec object --- cmdeploy/src/cmdeploy/cmdeploy.py | 6 +++--- cmdeploy/src/cmdeploy/dns.py | 23 ++++++----------------- cmdeploy/src/cmdeploy/sshexec.py | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/cmdeploy/src/cmdeploy/cmdeploy.py b/cmdeploy/src/cmdeploy/cmdeploy.py index d19e375e7..35c656f68 100644 --- a/cmdeploy/src/cmdeploy/cmdeploy.py +++ b/cmdeploy/src/cmdeploy/cmdeploy.py @@ -19,7 +19,7 @@ from termcolor import colored from . import dns, remote -from .sshexec import SSHExec +from .sshexec import SSHExec, LocalExec # # cmdeploy sub commands and options @@ -365,9 +365,9 @@ def get_parser(): def get_sshexec(ssh_host: str, verbose=True): if ssh_host in ["localhost", "@local"]: - return "localhost" + return LocalExec(verbose, docker=False) elif ssh_host == "docker": - return "docker" + return LocalExec(verbose, docker=True) if verbose: print(f"[ssh] login to {ssh_host}") return SSHExec(ssh_host, verbose=verbose) diff --git a/cmdeploy/src/cmdeploy/dns.py b/cmdeploy/src/cmdeploy/dns.py index 0562181a6..2d37084d8 100644 --- a/cmdeploy/src/cmdeploy/dns.py +++ b/cmdeploy/src/cmdeploy/dns.py @@ -7,15 +7,9 @@ def get_initial_remote_data(sshexec, mail_domain): - if sshexec == "localhost": - result = remote.rdns.perform_initial_checks(mail_domain) - elif sshexec == "docker": - result = remote.rdns.perform_initial_checks(mail_domain, pre_command="docker exec chatmail ") - else: - result = sshexec.logged( - call=remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=mail_domain) - ) - return result + return sshexec.logged( + call=remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=mail_domain) + ) def check_initial_remote_data(remote_data, *, print=print): @@ -50,14 +44,9 @@ def check_full_zone(sshexec, remote_data, out, zonefile) -> int: """Check existing DNS records, optionally write them to zone file and return (exitcode, remote_data) tuple.""" - if sshexec in ["localhost", "docker"]: - required_diff, recommended_diff = remote.rdns.check_zonefile( - zonefile=zonefile, verbose=False - ) - else: - required_diff, recommended_diff = sshexec.logged( - remote.rdns.check_zonefile, kwargs=dict(zonefile=zonefile, verbose=False), - ) + required_diff, recommended_diff = sshexec.logged( + remote.rdns.check_zonefile, kwargs=dict(zonefile=zonefile, verbose=False), + ) returncode = 0 if required_diff: diff --git a/cmdeploy/src/cmdeploy/sshexec.py b/cmdeploy/src/cmdeploy/sshexec.py index 400ce50d9..c8f85eee4 100644 --- a/cmdeploy/src/cmdeploy/sshexec.py +++ b/cmdeploy/src/cmdeploy/sshexec.py @@ -82,3 +82,19 @@ def logged(self, call, kwargs): res = self(call, kwargs, log_callback=remote.rshell.log_progress) print_stderr() return res + + +class LocalExec: + def __init__(self, verbose=False, docker=False): + self.verbose = verbose + self.docker = docker + + def logged(self, call, kwargs: dict): + where = "locally" + if self.docker: + if call == remote.rdns.perform_initial_checks: + kwargs['pre_command'] = "docker exec chatmail " + where = "in docker" + if self.verbose: + print(f"Running {where}: {call.__name__}(**{kwargs})") + return call(**kwargs) From a1c28709db878e7cca8e3d2dcc39d7dbf1ae4aaa Mon Sep 17 00:00:00 2001 From: missytake Date: Tue, 14 Oct 2025 20:44:06 +0200 Subject: [PATCH 5/5] cmdeploy: make --ssh-host expect '@docker' instead of 'docker' --- cmdeploy/src/cmdeploy/cmdeploy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmdeploy/src/cmdeploy/cmdeploy.py b/cmdeploy/src/cmdeploy/cmdeploy.py index 35c656f68..e71d0ce80 100644 --- a/cmdeploy/src/cmdeploy/cmdeploy.py +++ b/cmdeploy/src/cmdeploy/cmdeploy.py @@ -99,7 +99,7 @@ def run_cmd(args, out): pyinf = "pyinfra --dry" if args.dry_run else "pyinfra" cmd = f"{pyinf} --ssh-user root {ssh_host} {deploy_path} -y" - if ssh_host in ["localhost", "docker"]: + if ssh_host in ["localhost", "@docker"]: cmd = f"{pyinf} @local {deploy_path} -y" if version.parse(pyinfra.__version__) < version.parse("3"): @@ -366,7 +366,7 @@ def get_parser(): def get_sshexec(ssh_host: str, verbose=True): if ssh_host in ["localhost", "@local"]: return LocalExec(verbose, docker=False) - elif ssh_host == "docker": + elif ssh_host == "@docker": return LocalExec(verbose, docker=True) if verbose: print(f"[ssh] login to {ssh_host}")