"""Tests for systemd service and timer configuration""" import pytest testinfra_hosts = [ "borg-client", "borg-client-2", "borg-client-multi", "borg-client-nonroot", "borg-client-multi-keys", "borg-client-transition", ] CLIENT_CONFIGS = { "borg-client": { "user": "root", "server": "borg-server", "schedule": "*-*-* 02:00:00", "success_exit_status": False, "prune_enabled": True, "prune_timer": False, }, "borg-client-2": { "user": "root", "server": "borg-server-2", "schedule": "*-*-* 03:00:00", "success_exit_status": True, "prune_enabled": True, "prune_timer": True, "prune_schedule": "*-*-* 05:00:00", }, "borg-client-nonroot": { "user": "backupuser", "server": "borg-server", "schedule": "*-*-* 02:00:00", "success_exit_status": False, "prune_enabled": False, "prune_timer": False, }, "borg-client-transition": { "user": "root", "server": "transition-repo", "schedule": "*-*-* 02:00:00", "success_exit_status": False, "prune_enabled": True, "prune_timer": False, }, } MULTI_INSTANCE_CONFIGS = { "borg-client-multi": { "user": "root", "repos": ["configs", "home-data"], "schedules": { "configs": "*-*-* 02:00:00", "home-data": "*-*-* 04:00:00", }, }, "borg-client-multi-keys": { "user": "root", "repos": ["configs-keys", "home-data-keys"], "schedules": { "configs-keys": "*-*-* 02:00:00", "home-data-keys": "*-*-* 04:00:00", }, }, } @pytest.fixture def config(host): hostname = host.backend.hostname if hostname in CLIENT_CONFIGS: return {"type": "single", **CLIENT_CONFIGS[hostname]} return {"type": "multi", **MULTI_INSTANCE_CONFIGS[hostname]} class TestSystemdServiceFile: def test_service_file_exists(self, host, config): if config["type"] == "single": service = host.file( f"/etc/systemd/system/borg_backup@{config['server']}.service" ) assert service.exists assert service.user == "root" assert service.group == "root" assert service.mode == 0o644 else: for repo in config["repos"]: service = host.file(f"/etc/systemd/system/borg_backup@{repo}.service") assert service.exists assert service.user == "root" assert service.group == "root" def test_service_content(self, host, config): if config["type"] == "single": service = host.file( f"/etc/systemd/system/borg_backup@{config['server']}.service" ) assert service.contains("[Unit]") assert service.contains("[Service]") assert service.contains("[Install]") assert service.contains("Type=oneshot") assert service.contains( f"ExecStart=/usr/local/bin/run_borg_backup@{config['server']}" ) assert ( "ExecStart=/usr/local/bin/run_borg_backup\n" not in service.content_string ) if config["prune_enabled"] and not config["prune_timer"]: assert service.contains( f"OnSuccess=borg_prune@{config['server']}.service" ) else: assert not service.contains("OnSuccess=") else: for repo in config["repos"]: service = host.file(f"/etc/systemd/system/borg_backup@{repo}.service") assert service.contains("[Unit]") assert service.contains("[Service]") assert service.contains("[Install]") assert service.contains("Type=oneshot") assert service.contains( f"ExecStart=/usr/local/bin/run_borg_backup@{repo}" ) def test_service_user(self, host, config): if config["type"] == "single": service = host.file( f"/etc/systemd/system/borg_backup@{config['server']}.service" ) assert service.contains(f"User={config['user']}") assert service.contains(f"Group={config['user']}") else: for repo in config["repos"]: service = host.file(f"/etc/systemd/system/borg_backup@{repo}.service") assert service.contains(f"User={config['user']}") assert service.contains(f"Group={config['user']}") def test_success_exit_status(self, host, config): if config["type"] != "single": return service = host.file( f"/etc/systemd/system/borg_backup@{config['server']}.service" ) assert service.exists if config["success_exit_status"]: assert service.contains("SuccessExitStatus=1 TEMPFAIL") else: assert not service.contains("SuccessExitStatus=") class TestSystemdTimerFile: def test_timer_file_exists(self, host, config): if config["type"] == "single": timer = host.file( f"/etc/systemd/system/borg_backup@{config['server']}.timer" ) assert timer.exists assert timer.user == "root" assert timer.group == "root" assert timer.mode == 0o644 else: for repo in config["repos"]: timer = host.file(f"/etc/systemd/system/borg_backup@{repo}.timer") assert timer.exists def test_timer_content(self, host, config): if config["type"] == "single": timer = host.file( f"/etc/systemd/system/borg_backup@{config['server']}.timer" ) assert timer.contains("[Unit]") assert timer.contains("[Timer]") assert timer.contains("[Install]") assert timer.contains("OnCalendar=") assert timer.contains("AccuracySec=") else: for repo in config["repos"]: timer = host.file(f"/etc/systemd/system/borg_backup@{repo}.timer") assert timer.contains("[Unit]") assert timer.contains("[Timer]") assert timer.contains("[Install]") assert timer.contains("OnCalendar=") assert timer.contains("AccuracySec=") def test_timer_schedule(self, host, config): if config["type"] == "single": timer = host.file( f"/etc/systemd/system/borg_backup@{config['server']}.timer" ) assert f"OnCalendar={config['schedule']}" in timer.content_string else: for repo in config["repos"]: timer = host.file(f"/etc/systemd/system/borg_backup@{repo}.timer") assert f"OnCalendar={config['schedules'][repo]}" in timer.content_string class TestSystemdState: def test_timer_enabled(self, host, config): if config["type"] == "single": timer_name = f"borg_backup@{config['server']}.timer" c = host.run(f"systemctl is-enabled {timer_name}") assert c.rc == 0 assert c.stdout.strip() == "enabled" else: for repo in config["repos"]: c = host.run(f"systemctl is-enabled borg_backup@{repo}.timer") assert c.rc == 0 assert c.stdout.strip() == "enabled" def test_timer_active(self, host, config): if config["type"] == "single": timer_name = f"borg_backup@{config['server']}.timer" c = host.run(f"systemctl is-active {timer_name}") assert c.rc == 0 else: for repo in config["repos"]: c = host.run(f"systemctl is-active borg_backup@{repo}.timer") assert c.rc == 0 def test_daemon_reload_ok(self, host, config): c = host.run("systemctl daemon-reload") assert c.rc == 0 class TestPruneSystemd: def test_prune_service_created_when_pruning_enabled(self, host, config): if config["type"] != "single": return service = host.file( f"/etc/systemd/system/borg_prune@{config['server']}.service" ) assert service.exists == config["prune_enabled"] if config["prune_enabled"]: assert ( f"ExecStart=/usr/local/bin/run_borg_prune@{config['server']}" in service.content_string ) def test_prune_timer_created_when_configured(self, host, config): if config["type"] != "single" or not config.get("prune_timer"): return timer = host.file(f"/etc/systemd/system/borg_prune@{config['server']}.timer") service = host.file( f"/etc/systemd/system/borg_prune@{config['server']}.service" ) assert timer.exists assert service.exists assert f"OnCalendar={config['prune_schedule']}" in timer.content_string assert ( f"ExecStart=/usr/local/bin/run_borg_prune@{config['server']}" in service.content_string ) assert "ExecStart=/usr/local/bin/run_borg_prune\n" not in service.content_string assert "SuccessExitStatus=1 TEMPFAIL" in service.content_string def test_prune_timer_not_created_for_after_backup(self, host, config): if config["type"] != "single" or config.get("prune_timer"): return timer = host.file(f"/etc/systemd/system/borg_prune@{config['server']}.timer") assert not timer.exists def test_transition_host_stale_prune_timer_removed(self, host): hostname = host.backend.get_hostname() if hostname != "borg-client-transition": return timer = host.file("/etc/systemd/system/borg_prune@transition-repo.timer") service = host.file("/etc/systemd/system/borg_prune@transition-repo.service") assert not timer.exists assert service.exists