diff options
| author | Colin Wilk <colin@wilk.cx> | 2026-06-27 23:01:10 +0200 |
|---|---|---|
| committer | Colin Wilk <colin@wilk.cx> | 2026-06-27 23:05:21 +0200 |
| commit | 8d872069976c5445c4396804ef3a0196f10eb14b (patch) | |
| tree | 53ba2a34baf356fa9711e9be472d2d5322f1010e /molecule | |
| parent | cce7d2d258292c283d64ce8da14a6d1e366b564d (diff) | |
| download | ansible-role-borgbackup-8d872069976c5445c4396804ef3a0196f10eb14b.tar.gz ansible-role-borgbackup-8d872069976c5445c4396804ef3a0196f10eb14b.zip | |
feat: add per-repo SSH key support
Add borg_ssh_key_per_repo option to generate unique SSH keypairs per
(server, repo) combination. When enabled, each repository gets its own
authorized_keys entry, enabling:
- Independent --append-only settings per repository
- Per-repo storage quotas
Diffstat (limited to 'molecule')
| -rw-r--r-- | molecule/default/converge.yml | 38 | ||||
| -rw-r--r-- | molecule/default/molecule.yml | 8 | ||||
| -rw-r--r-- | molecule/default/tests/test_client_setup.py | 117 | ||||
| -rw-r--r-- | molecule/default/tests/test_server_setup.py | 21 | ||||
| -rw-r--r-- | molecule/default/tests/test_ssh_connectivity.py | 45 | ||||
| -rw-r--r-- | molecule/default/tests/test_systemd.py | 59 |
6 files changed, 270 insertions, 18 deletions
diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index d90f33a..fec5f95 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -5,6 +5,7 @@ - borg-client-2 - borg-client-multi - borg-client-nonroot + - borg-client-multi-keys vars: borg_server_host: borg-server @@ -127,3 +128,40 @@ borg_included_dirs: - /etc borg_excluded_dirs: [] + +- name: Converge - Per-repo SSH keys with different append-only settings + hosts: borg-client-multi-keys + serial: 1 + + vars: + borg_server_host: borg-server + borg_server_user_home: /opt/borg + borg_decryption_keys_yaml_path: "{{ playbook_dir }}/decryption_keys.yml" + + roles: + - role: kliwniloc.borgbackup + vars: + borg_repo_name: configs-keys + borg_backup_argument: configs-keys + borg_ssh_key_per_repo: true + borg_ssh_key_type: ed25519 + borg_mode_append_only: true + borg_compression: zstd + borg_included_dirs: + - /etc + borg_excluded_dirs: [] + borg_systemd_oncalendar: "*-*-* 02:00:00" + + - role: kliwniloc.borgbackup + vars: + borg_repo_name: home-data-keys + borg_backup_argument: home-data-keys + borg_ssh_key_per_repo: true + borg_ssh_key_type: ed25519 + borg_mode_append_only: false + borg_compression: lz4 + borg_included_dirs: + - /home + borg_excluded_dirs: + - /home/*/.cache + borg_systemd_oncalendar: "*-*-* 04:00:00" diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml index b4f68cb..3270a63 100644 --- a/molecule/default/molecule.yml +++ b/molecule/default/molecule.yml @@ -43,6 +43,14 @@ platforms: networks: - name: molecule-container-net + - name: borg-client-multi-keys + image: ${MOLECULE_DISTRO_CLIENT:-debian:12} + dockerfile: Dockerfile.j2 + pre_build_image: false + privileged: true + networks: + - name: molecule-container-net + - name: borg-server image: ${MOLECULE_DISTRO_SERVER:-debian:12} dockerfile: Dockerfile.j2 diff --git a/molecule/default/tests/test_client_setup.py b/molecule/default/tests/test_client_setup.py index 290e3dc..a0e4f4a 100644 --- a/molecule/default/tests/test_client_setup.py +++ b/molecule/default/tests/test_client_setup.py @@ -1,7 +1,7 @@ """Tests for client setup configuration""" import pytest -testinfra_hosts = ['borg-client', 'borg-client-2', 'borg-client-multi', 'borg-client-nonroot'] +testinfra_hosts = ['borg-client', 'borg-client-2', 'borg-client-multi', 'borg-client-nonroot', 'borg-client-multi-keys'] CLIENT_USER_MAP = { @@ -9,6 +9,7 @@ CLIENT_USER_MAP = { 'borg-client-2': 'root', 'borg-client-multi': 'root', 'borg-client-nonroot': 'backupuser', + 'borg-client-multi-keys': 'root', } CLIENT_SSH_KEY_TYPE_MAP = { @@ -16,6 +17,15 @@ CLIENT_SSH_KEY_TYPE_MAP = { 'borg-client-2': 'ed25519', 'borg-client-multi': 'rsa', 'borg-client-nonroot': 'rsa', + 'borg-client-multi-keys': 'ed25519', +} + +CLIENT_SSH_KEY_PER_REPO_MAP = { + 'borg-client': False, + 'borg-client-2': False, + 'borg-client-multi': False, + 'borg-client-nonroot': False, + 'borg-client-multi-keys': True, } @@ -29,6 +39,11 @@ def get_client_ssh_key_type(host): return CLIENT_SSH_KEY_TYPE_MAP.get(hostname, 'rsa') +def get_client_ssh_key_per_repo(host): + hostname = host.backend.get_hostname() + return CLIENT_SSH_KEY_PER_REPO_MAP.get(hostname, False) + + def get_client_home(host): user = get_client_user(host) return f'/home/{user}' if user != 'root' else '/root' @@ -49,20 +64,48 @@ class TestSSHSetup: client_home = get_client_home(host) client_user = get_client_user(host) key_type = get_client_ssh_key_type(host) - key = host.file(f'{client_home}/.ssh/id_{key_type}') - assert key.exists - assert key.user == client_user - assert key.group == client_user - assert key.mode == 0o600 + per_repo = get_client_ssh_key_per_repo(host) + hostname = host.backend.get_hostname() + + if per_repo and hostname == 'borg-client-multi-keys': + key1 = host.file(f'{client_home}/.ssh/id_{key_type}_borgbackup_borg_server_configs_keys') + key2 = host.file(f'{client_home}/.ssh/id_{key_type}_borgbackup_borg_server_home_data_keys') + assert key1.exists + assert key1.user == client_user + assert key1.group == client_user + assert key1.mode == 0o600 + assert key2.exists + assert key2.user == client_user + assert key2.group == client_user + assert key2.mode == 0o600 + else: + key = host.file(f'{client_home}/.ssh/id_{key_type}') + assert key.exists + assert key.user == client_user + assert key.group == client_user + assert key.mode == 0o600 def test_ssh_public_key_exists(self, host): client_home = get_client_home(host) client_user = get_client_user(host) key_type = get_client_ssh_key_type(host) - key = host.file(f'{client_home}/.ssh/id_{key_type}.pub') - assert key.exists - assert key.user == client_user - assert key.group == client_user + per_repo = get_client_ssh_key_per_repo(host) + hostname = host.backend.get_hostname() + + if per_repo and hostname == 'borg-client-multi-keys': + key1 = host.file(f'{client_home}/.ssh/id_{key_type}_borgbackup_borg_server_configs_keys.pub') + key2 = host.file(f'{client_home}/.ssh/id_{key_type}_borgbackup_borg_server_home_data_keys.pub') + assert key1.exists + assert key1.user == client_user + assert key1.group == client_user + assert key2.exists + assert key2.user == client_user + assert key2.group == client_user + else: + key = host.file(f'{client_home}/.ssh/id_{key_type}.pub') + assert key.exists + assert key.user == client_user + assert key.group == client_user def test_known_hosts_contains_borg_server(self, host): client_home = get_client_home(host) @@ -87,6 +130,17 @@ class TestBackupScript: assert script2.user == client_user assert script2.group == client_user assert script2.mode == 0o711 + elif hostname == 'borg-client-multi-keys': + script1 = host.file('/usr/local/bin/run_borg_backup@configs-keys') + script2 = host.file('/usr/local/bin/run_borg_backup@home-data-keys') + assert script1.exists + assert script1.user == client_user + assert script1.group == client_user + assert script1.mode == 0o711 + assert script2.exists + assert script2.user == client_user + assert script2.group == client_user + assert script2.mode == 0o711 elif hostname == 'borg-client-nonroot': script = host.file('/usr/local/bin/run_borg_backup@borg-server') assert script.exists @@ -110,6 +164,11 @@ class TestBackupScript: script2 = host.file('/usr/local/bin/run_borg_backup@home-data') assert script1.contains('borg create') assert script2.contains('borg create') + elif hostname == 'borg-client-multi-keys': + script1 = host.file('/usr/local/bin/run_borg_backup@configs-keys') + script2 = host.file('/usr/local/bin/run_borg_backup@home-data-keys') + assert script1.contains('borg create') + assert script2.contains('borg create') elif hostname == 'borg-client-nonroot': script = host.file('/usr/local/bin/run_borg_backup@borg-server') assert script.contains('borg create') @@ -127,6 +186,11 @@ class TestBackupScript: script2 = host.file('/usr/local/bin/run_borg_backup@home-data') assert script1.contains('-C zstd') assert script2.contains('-C lz4') + elif hostname == 'borg-client-multi-keys': + script1 = host.file('/usr/local/bin/run_borg_backup@configs-keys') + script2 = host.file('/usr/local/bin/run_borg_backup@home-data-keys') + assert script1.contains('-C zstd') + assert script2.contains('-C lz4') elif hostname == 'borg-client-2': script = host.file('/usr/local/bin/run_borg_backup') assert script.contains('-C') @@ -148,6 +212,13 @@ class TestBackupScript: assert script1.contains('/opt/borg/configs') assert script2.contains('borg@borg-server') assert script2.contains('/opt/borg/home-data') + elif hostname == 'borg-client-multi-keys': + script1 = host.file('/usr/local/bin/run_borg_backup@configs-keys') + script2 = host.file('/usr/local/bin/run_borg_backup@home-data-keys') + assert script1.contains('borg@borg-server') + assert script1.contains('/opt/borg/configs-keys') + assert script2.contains('borg@borg-server') + assert script2.contains('/opt/borg/home-data-keys') elif hostname == 'borg-client-nonroot': script = host.file('/usr/local/bin/run_borg_backup@borg-server') assert script.contains('borg@borg-server') @@ -168,6 +239,12 @@ class TestBackupScript: assert '/etc' in script1.content_string assert '/home' in script2.content_string assert '--exclude' in script2.content_string + elif hostname == 'borg-client-multi-keys': + script1 = host.file('/usr/local/bin/run_borg_backup@configs-keys') + script2 = host.file('/usr/local/bin/run_borg_backup@home-data-keys') + assert '/etc' in script1.content_string + assert '/home' in script2.content_string + assert '--exclude' in script2.content_string elif hostname == 'borg-client-nonroot': script = host.file('/usr/local/bin/run_borg_backup@borg-server') content = script.content_string @@ -187,6 +264,11 @@ class TestBackupScript: script2 = host.file('/usr/local/bin/run_borg_backup@home-data') assert script1.mode == 0o711 assert script2.mode == 0o711 + elif hostname == 'borg-client-multi-keys': + script1 = host.file('/usr/local/bin/run_borg_backup@configs-keys') + script2 = host.file('/usr/local/bin/run_borg_backup@home-data-keys') + assert script1.mode == 0o711 + assert script2.mode == 0o711 elif hostname == 'borg-client-nonroot': script = host.file('/usr/local/bin/run_borg_backup@borg-server') assert script.mode == 0o711 @@ -196,6 +278,21 @@ class TestBackupScript: else: pytest.fail(f"Unexpected hostname: {hostname}") + def test_backup_script_contains_borg_rsh_when_per_repo(self, host): + hostname = host.backend.get_hostname() + per_repo = get_client_ssh_key_per_repo(host) + + if not per_repo: + return + + if hostname == 'borg-client-multi-keys': + script1 = host.file('/usr/local/bin/run_borg_backup@configs-keys') + script2 = host.file('/usr/local/bin/run_borg_backup@home-data-keys') + assert 'BORG_RSH' in script1.content_string + assert 'BORG_RSH' in script2.content_string + assert 'ssh -i' in script1.content_string + assert 'ssh -i' in script2.content_string + class TestMultiInstanceBaseScript: def test_base_script_exists(self, host): diff --git a/molecule/default/tests/test_server_setup.py b/molecule/default/tests/test_server_setup.py index 3c494f7..5ec1520 100644 --- a/molecule/default/tests/test_server_setup.py +++ b/molecule/default/tests/test_server_setup.py @@ -1,5 +1,6 @@ """Tests for borg server setup and repository configuration""" import pytest +import re testinfra_hosts = ['borg-server'] @@ -62,8 +63,13 @@ class TestBorgSSHSetup: def test_authorized_keys_multi_instance_repo_count(self, host): auth_keys = host.file('/opt/borg/.ssh/authorized_keys') content = auth_keys.content_string - configs_count = content.count('/opt/borg/configs') - home_data_count = content.count('/opt/borg/home-data') + + restricted_repos = re.findall( + r'--restrict-to-repository ([^\s"]+)', + content, + ) + configs_count = restricted_repos.count('/opt/borg/configs') + home_data_count = restricted_repos.count('/opt/borg/home-data') assert configs_count == 1, f"configs repo should appear once in authorized_keys, found {configs_count}" assert home_data_count == 1, f"home-data repo should appear once in authorized_keys, found {home_data_count}" @@ -71,11 +77,18 @@ class TestBorgSSHSetup: def test_authorized_keys_format_valid(self, host): auth_keys = host.file('/opt/borg/.ssh/authorized_keys') content = auth_keys.content_string + valid_hosts = ( + 'borg-client', + 'borg-client-2', + 'borg-client-multi', + 'borg-client-nonroot', + 'borg-client-multi-keys', + ) for line in content.split('\n'): if not line.strip(): continue assert line.startswith('restrict,command="borg serve'), f"Line should start with restrict,command: {line[:50]}" - assert '@' in line and line.rstrip().endswith(('borg-client', 'borg-client-2', 'borg-client-multi', 'borg-client-nonroot')), f"Line should contain user@hostname marker: {line[-40:]}" + assert '@' in line and line.rstrip().endswith(valid_hosts), f"Line should contain user@hostname marker: {line[-40:]}" assert '--restrict-to-repository' in line, f"Line should have repo restriction: {line[:80]}" def test_authorized_keys_multi_instance_no_cross_host_repos(self, host): @@ -85,7 +98,7 @@ class TestBorgSSHSetup: multi_lines = [ line for line in content.split('\n') - if line and 'root@borg-client-multi' in line + if line.rstrip().endswith('root@borg-client-multi') ] assert len(multi_lines) == 1, ( f"Should have exactly one entry for borg-client-multi, found {len(multi_lines)}" diff --git a/molecule/default/tests/test_ssh_connectivity.py b/molecule/default/tests/test_ssh_connectivity.py index 89b24a5..0cc1897 100644 --- a/molecule/default/tests/test_ssh_connectivity.py +++ b/molecule/default/tests/test_ssh_connectivity.py @@ -1,7 +1,19 @@ """Tests for SSH connectivity between client and server""" import pytest -testinfra_hosts = ['borg-client', 'borg-client-2', 'borg-client-multi'] +testinfra_hosts = ['borg-client', 'borg-client-2', 'borg-client-multi', 'borg-client-multi-keys'] + + +def get_borg_rsh(host, repo_name): + hostname = host.backend.get_hostname() + if hostname != 'borg-client-multi-keys': + return '' + + sanitized_repo_name = repo_name.replace('-', '_') + return ( + 'BORG_RSH="ssh -i ' + f'/root/.ssh/id_ed25519_borgbackup_borg_server_{sanitized_repo_name}"' + ) def test_ssh_connection_to_server(host): @@ -10,7 +22,19 @@ def test_ssh_connection_to_server(host): def test_no_password_prompt_on_connect(host): - c = host.run('ssh -o BatchMode=yes -o PreferredAuthentications=publickey borg@borg-server exit') + hostname = host.backend.get_hostname() + + if hostname == 'borg-client-multi-keys': + c = host.run( + 'ssh -i /root/.ssh/id_ed25519_borgbackup_borg_server_configs_keys ' + '-o BatchMode=yes -o PreferredAuthentications=publickey ' + 'borg@borg-server exit' + ) + else: + c = host.run( + 'ssh -o BatchMode=yes -o PreferredAuthentications=publickey ' + 'borg@borg-server exit' + ) assert c.rc == 0 @@ -27,6 +51,12 @@ def test_borg_can_connect_to_server(host): if hostname == 'borg-client-multi': c = host.run('borg list borg@borg-server:/opt/borg/configs') assert c.rc == 0, 'Should be able to connect to configs repo' + elif hostname == 'borg-client-multi-keys': + c = host.run( + f'{get_borg_rsh(host, "configs-keys")} ' + 'borg list borg@borg-server:/opt/borg/configs-keys' + ) + assert c.rc == 0, 'Should be able to connect to configs-keys repo' elif hostname == 'borg-client': c = host.run('borg list borg@borg-server:/opt/borg/borg-client') assert c.rc == 0 @@ -45,6 +75,17 @@ def test_borg_info_works(host): assert c.rc == 0, 'Should be able to get info for configs repo' c2 = host.run('borg info borg@borg-server:/opt/borg/home-data') assert c2.rc == 0, 'Should be able to get info for home-data repo' + elif hostname == 'borg-client-multi-keys': + c = host.run( + f'{get_borg_rsh(host, "configs-keys")} ' + 'borg info borg@borg-server:/opt/borg/configs-keys' + ) + assert c.rc == 0, 'Should be able to get info for configs-keys repo' + c2 = host.run( + f'{get_borg_rsh(host, "home-data-keys")} ' + 'borg info borg@borg-server:/opt/borg/home-data-keys' + ) + assert c2.rc == 0, 'Should be able to get info for home-data-keys repo' elif hostname == 'borg-client': c = host.run('borg info borg@borg-server:/opt/borg/borg-client') assert c.rc == 0 diff --git a/molecule/default/tests/test_systemd.py b/molecule/default/tests/test_systemd.py index 7b7b2ab..b713c27 100644 --- a/molecule/default/tests/test_systemd.py +++ b/molecule/default/tests/test_systemd.py @@ -2,7 +2,7 @@ import pytest -testinfra_hosts = ['borg-client', 'borg-client-2', 'borg-client-multi', 'borg-client-nonroot'] +testinfra_hosts = ['borg-client', 'borg-client-2', 'borg-client-multi', 'borg-client-nonroot', 'borg-client-multi-keys'] CLIENT_USER_MAP = { @@ -10,6 +10,7 @@ CLIENT_USER_MAP = { 'borg-client-2': 'root', 'borg-client-multi': 'root', 'borg-client-nonroot': 'backupuser', + 'borg-client-multi-keys': 'root', } @@ -31,6 +32,15 @@ class TestSystemdServiceFile: assert service2.exists assert service2.user == 'root' assert service2.group == 'root' + elif hostname == 'borg-client-multi-keys': + service1 = host.file('/etc/systemd/system/borg_backup@configs-keys.service') + service2 = host.file('/etc/systemd/system/borg_backup@home-data-keys.service') + assert service1.exists + assert service1.user == 'root' + assert service1.group == 'root' + assert service2.exists + assert service2.user == 'root' + assert service2.group == 'root' elif hostname == 'borg-client-nonroot': service = host.file('/etc/systemd/system/borg_backup@borg-server.service') assert service.exists @@ -57,6 +67,14 @@ class TestSystemdServiceFile: assert service.contains('[Service]') assert service.contains('[Install]') assert service.contains('Type=oneshot') + elif hostname == 'borg-client-multi-keys': + service1 = host.file('/etc/systemd/system/borg_backup@configs-keys.service') + service2 = host.file('/etc/systemd/system/borg_backup@home-data-keys.service') + for service in [service1, service2]: + assert service.contains('[Unit]') + assert service.contains('[Service]') + assert service.contains('[Install]') + assert service.contains('Type=oneshot') elif hostname == 'borg-client-nonroot': service = host.file('/etc/systemd/system/borg_backup@borg-server.service') assert service.contains('[Unit]') @@ -84,6 +102,12 @@ class TestSystemdServiceFile: for service in [service1, service2]: assert service.contains(f'User={client_user}') assert service.contains(f'Group={client_user}') + elif hostname == 'borg-client-multi-keys': + service1 = host.file('/etc/systemd/system/borg_backup@configs-keys.service') + service2 = host.file('/etc/systemd/system/borg_backup@home-data-keys.service') + for service in [service1, service2]: + assert service.contains(f'User={client_user}') + assert service.contains(f'Group={client_user}') elif hostname in ('borg-client', 'borg-client-2', 'borg-client-nonroot'): service = host.file('/etc/systemd/system/borg_backup@borg-server.service') assert service.contains(f'User={client_user}') @@ -93,7 +117,7 @@ class TestSystemdServiceFile: def test_success_exit_status(self, host): hostname = host.backend.get_hostname() - if hostname in ('borg-client-multi', 'borg-client-nonroot'): + if hostname in ('borg-client-multi', 'borg-client-nonroot', 'borg-client-multi-keys'): return service = host.file('/etc/systemd/system/borg_backup@borg-server.service') @@ -123,6 +147,11 @@ class TestSystemdTimerFile: timer2 = host.file('/etc/systemd/system/borg_backup@home-data.timer') assert timer1.exists assert timer2.exists + elif hostname == 'borg-client-multi-keys': + timer1 = host.file('/etc/systemd/system/borg_backup@configs-keys.timer') + timer2 = host.file('/etc/systemd/system/borg_backup@home-data-keys.timer') + assert timer1.exists + assert timer2.exists elif hostname in ('borg-client', 'borg-client-2', 'borg-client-nonroot'): timer = host.file('/etc/systemd/system/borg_backup@borg-server.timer') assert timer.exists @@ -144,6 +173,15 @@ class TestSystemdTimerFile: assert timer.contains('[Install]') assert timer.contains('OnCalendar=') assert timer.contains('AccuracySec=') + elif hostname == 'borg-client-multi-keys': + timer1 = host.file('/etc/systemd/system/borg_backup@configs-keys.timer') + timer2 = host.file('/etc/systemd/system/borg_backup@home-data-keys.timer') + for timer in [timer1, timer2]: + assert timer.contains('[Unit]') + assert timer.contains('[Timer]') + assert timer.contains('[Install]') + assert timer.contains('OnCalendar=') + assert timer.contains('AccuracySec=') elif hostname in ('borg-client', 'borg-client-2', 'borg-client-nonroot'): timer = host.file('/etc/systemd/system/borg_backup@borg-server.timer') assert timer.contains('[Unit]') @@ -162,6 +200,11 @@ class TestSystemdTimerFile: timer2 = host.file('/etc/systemd/system/borg_backup@home-data.timer') assert 'OnCalendar=*-*-* 02:00:00' in timer1.content_string assert 'OnCalendar=*-*-* 04:00:00' in timer2.content_string + elif hostname == 'borg-client-multi-keys': + timer1 = host.file('/etc/systemd/system/borg_backup@configs-keys.timer') + timer2 = host.file('/etc/systemd/system/borg_backup@home-data-keys.timer') + assert 'OnCalendar=*-*-* 02:00:00' in timer1.content_string + assert 'OnCalendar=*-*-* 04:00:00' in timer2.content_string elif hostname == 'borg-client-2': timer = host.file('/etc/systemd/system/borg_backup@borg-server.timer') assert 'OnCalendar=*-*-* 03:00:00' in timer.content_string @@ -183,6 +226,13 @@ class TestSystemdState: assert c1.stdout.strip() == 'enabled' assert c2.rc == 0 assert c2.stdout.strip() == 'enabled' + elif hostname == 'borg-client-multi-keys': + c1 = host.run('systemctl is-enabled borg_backup@configs-keys.timer') + c2 = host.run('systemctl is-enabled borg_backup@home-data-keys.timer') + assert c1.rc == 0 + assert c1.stdout.strip() == 'enabled' + assert c2.rc == 0 + assert c2.stdout.strip() == 'enabled' elif hostname in ('borg-client', 'borg-client-2', 'borg-client-nonroot'): timer_name = 'borg_backup@borg-server.timer' c = host.run(f"systemctl is-enabled {timer_name}") @@ -199,6 +249,11 @@ class TestSystemdState: c2 = host.run('systemctl is-active borg_backup@home-data.timer') assert c1.rc == 0 assert c2.rc == 0 + elif hostname == 'borg-client-multi-keys': + c1 = host.run('systemctl is-active borg_backup@configs-keys.timer') + c2 = host.run('systemctl is-active borg_backup@home-data-keys.timer') + assert c1.rc == 0 + assert c2.rc == 0 elif hostname in ('borg-client', 'borg-client-2', 'borg-client-nonroot'): timer_name = 'borg_backup@borg-server.timer' c = host.run(f"systemctl is-active {timer_name}") |