diff options
Diffstat (limited to 'molecule/default/tests/test_client_setup.py')
| -rw-r--r-- | molecule/default/tests/test_client_setup.py | 329 |
1 files changed, 254 insertions, 75 deletions
diff --git a/molecule/default/tests/test_client_setup.py b/molecule/default/tests/test_client_setup.py index 78e751a..231b946 100644 --- a/molecule/default/tests/test_client_setup.py +++ b/molecule/default/tests/test_client_setup.py @@ -1,5 +1,7 @@ """Tests for client setup configuration""" +import shlex + import pytest testinfra_hosts = [ @@ -8,6 +10,7 @@ testinfra_hosts = [ "borg-client-multi", "borg-client-nonroot", "borg-client-multi-keys", + "borg-client-transition", ] @@ -17,6 +20,7 @@ CLIENT_USER_MAP = { "borg-client-multi": "root", "borg-client-nonroot": "backupuser", "borg-client-multi-keys": "root", + "borg-client-transition": "root", } CLIENT_SSH_KEY_TYPE_MAP = { @@ -25,6 +29,7 @@ CLIENT_SSH_KEY_TYPE_MAP = { "borg-client-multi": "rsa", "borg-client-nonroot": "rsa", "borg-client-multi-keys": "ed25519", + "borg-client-transition": "rsa", } CLIENT_SSH_KEY_PER_REPO_MAP = { @@ -33,6 +38,7 @@ CLIENT_SSH_KEY_PER_REPO_MAP = { "borg-client-multi": False, "borg-client-nonroot": False, "borg-client-multi-keys": True, + "borg-client-transition": False, } @@ -56,6 +62,76 @@ def get_client_home(host): return f"/home/{user}" if user != "root" else "/root" +def get_backup_script_paths(hostname): + if hostname == "borg-client-multi": + return [ + "/usr/local/bin/run_borg_backup@configs", + "/usr/local/bin/run_borg_backup@home-data", + ] + if hostname == "borg-client-multi-keys": + return [ + "/usr/local/bin/run_borg_backup@configs-keys", + "/usr/local/bin/run_borg_backup@home-data-keys", + ] + if hostname == "borg-client-nonroot": + return ["/usr/local/bin/run_borg_backup@borg-server"] + if hostname == "borg-client": + return ["/usr/local/bin/run_borg_backup@borg-server"] + if hostname == "borg-client-2": + return ["/usr/local/bin/run_borg_backup@borg-server-2"] + if hostname == "borg-client-transition": + return ["/usr/local/bin/run_borg_backup@transition-repo"] + return [] + + +def get_prune_script_paths(hostname): + if hostname == "borg-client": + return ["/usr/local/bin/run_borg_prune@borg-server"] + if hostname == "borg-client-2": + return ["/usr/local/bin/run_borg_prune@borg-server-2"] + if hostname == "borg-client-transition": + return ["/usr/local/bin/run_borg_prune@transition-repo"] + return [] + + +def run_script_with_fake_borg(host, script_path, failed_command, failed_status): + """Run a generated script with a fake borg and return its result and calls.""" + temp_dir_result = host.run("mktemp -d /tmp/borg-exit-test.XXXXXX") + assert temp_dir_result.rc == 0 + temp_dir = temp_dir_result.stdout.strip() + fake_borg_path = f"{temp_dir}/borg" + log_path = f"{temp_dir}/calls" + + fake_borg = """#!/bin/bash +printf '%s\\n' "$1" >> "${BORG_TEST_LOG}" +if [ "$1" = "${BORG_FAIL_COMMAND}" ]; then + exit "${BORG_FAIL_STATUS}" +fi +exit 0 +""" + setup = host.run( + f"cat > {shlex.quote(fake_borg_path)} <<'EOF'\n" + f"{fake_borg}" + "EOF\n" + f"chmod 0755 {shlex.quote(fake_borg_path)}" + ) + assert setup.rc == 0 + + try: + result = host.run( + "env " + f"BORG_FAIL_COMMAND={shlex.quote(failed_command)} " + f"BORG_FAIL_STATUS={failed_status} " + f"BORG_TEST_LOG={shlex.quote(log_path)} " + f"PATH={shlex.quote(temp_dir)}:$PATH " + f"{shlex.quote(script_path)}" + ) + calls = host.file(log_path).content_string.splitlines() + return result, calls + finally: + host.run(f"rm -rf {shlex.quote(temp_dir)}") + + class TestSSHSetup: def test_ssh_directory_exists(self, host): client_home = get_client_home(host) @@ -162,8 +238,8 @@ class TestBackupScript: assert script.user == client_user assert script.group == client_user assert script.mode == 0o711 - elif hostname in ("borg-client", "borg-client-2"): - script = host.file("/usr/local/bin/run_borg_backup") + elif hostname in ("borg-client", "borg-client-2", "borg-client-transition"): + script = host.file(get_backup_script_paths(hostname)[0]) assert script.exists assert script.user == client_user assert script.group == client_user @@ -187,12 +263,156 @@ class TestBackupScript: elif hostname == "borg-client-nonroot": script = host.file("/usr/local/bin/run_borg_backup@borg-server") assert script.contains("borg create") - elif hostname in ("borg-client", "borg-client-2"): - script = host.file("/usr/local/bin/run_borg_backup") + elif hostname in ("borg-client", "borg-client-2", "borg-client-transition"): + script = host.file(get_backup_script_paths(hostname)[0]) assert script.contains("borg create") + assert not script.contains("borg prune") + assert not script.contains("borg compact") else: pytest.fail(f"Unexpected hostname: {hostname}") + +class TestPruneScript: + def test_prune_script_created_when_pruning_enabled(self, host): + hostname = host.backend.get_hostname() + if hostname not in ("borg-client", "borg-client-2", "borg-client-transition"): + return + + for script_path in get_prune_script_paths(hostname): + script = host.file(script_path) + assert script.exists + assert script.mode == 0o711 + assert script.contains("borg prune") + assert script.contains("borg compact") + + def test_scripts_have_valid_shell_syntax(self, host): + hostname = host.backend.get_hostname() + + for script_path in get_backup_script_paths(hostname) + get_prune_script_paths( + hostname + ): + result = host.run(f"bash -n {script_path}") + assert result.rc == 0 + assert "ANSIBLE MANAGED BLOCK" not in host.file(script_path).content_string + + def test_after_backup_prune_script_flow_and_options(self, host): + hostname = host.backend.get_hostname() + if hostname != "borg-client": + return + + backup = host.file("/usr/local/bin/run_borg_backup@borg-server") + prune = host.file("/usr/local/bin/run_borg_prune@borg-server") + content = prune.content_string + + assert "borg create" in backup.content_string + assert "borg prune" not in backup.content_string + assert "borg compact" not in backup.content_string + + prune_index = content.index("borg prune") + prune_status_index = content.index("borg_prune_exit=$?") + prune_guard_index = content.index('if [ "${borg_prune_exit}" -ne 0 ]; then') + compact_index = content.index("borg compact") + + assert prune_index < prune_status_index < prune_guard_index < compact_index + assert "--checkpoint-interval 1800" in content + assert "--glob-archives '{hostname}-*'" in content + assert "--keep-last 2" in content + assert "--stats" in content + assert "--threshold 1" in content + + def test_timer_prune_script_flow_and_options(self, host): + hostname = host.backend.get_hostname() + if hostname != "borg-client-2": + return + + backup_script = host.file("/usr/local/bin/run_borg_backup@borg-server-2") + prune_script = host.file("/usr/local/bin/run_borg_prune@borg-server-2") + prune_content = prune_script.content_string + + assert "borg prune" not in backup_script.content_string + assert "borg compact" not in backup_script.content_string + + prune_index = prune_content.index("borg prune") + prune_status_index = prune_content.index("borg_prune_exit=$?") + prune_guard_index = prune_content.index( + 'if [ "${borg_prune_exit}" -ne 0 ]; then' + ) + compact_index = prune_content.index("borg compact") + + assert prune_index < prune_status_index < prune_guard_index < compact_index + assert "--checkpoint-interval 1800" in prune_content + assert "--glob-archives '{hostname}-*'" in prune_content + assert "--keep-daily 7" in prune_content + assert "--keep-weekly 4" in prune_content + assert "--stats" in prune_content + assert "--threshold 10" in prune_content + + def test_transition_host_uses_separate_prune_script(self, host): + hostname = host.backend.get_hostname() + if hostname != "borg-client-transition": + return + + backup = host.file("/usr/local/bin/run_borg_backup@transition-repo") + prune = host.file("/usr/local/bin/run_borg_prune@transition-repo") + + assert "borg create" in backup.content_string + assert "borg prune" not in backup.content_string + assert "borg prune" in prune.content_string + assert "borg compact" in prune.content_string + assert "--keep-last 2" in prune.content_string + assert "--glob-archives '{hostname}-*'" in prune.content_string + + def test_backup_propagates_create_failure_status(self, host): + if host.backend.get_hostname() != "borg-client": + return + + result, calls = run_script_with_fake_borg( + host, + "/usr/local/bin/run_borg_backup@borg-server", + "create", + 42, + ) + + assert result.rc == 42 + assert calls == ["create"] + + def test_after_backup_preserves_borg_warning_status(self, host): + if host.backend.get_hostname() != "borg-client": + return + + result, calls = run_script_with_fake_borg( + host, + "/usr/local/bin/run_borg_backup@borg-server", + "create", + 1, + ) + + assert result.rc == 1 + assert calls == ["create"] + + @pytest.mark.parametrize( + ("failed_command", "failed_status", "expected_calls"), + [ + ("prune", 43, ["prune"]), + ("compact", 44, ["prune", "compact"]), + ], + ) + def test_timer_prune_propagates_failure_status( + self, host, failed_command, failed_status, expected_calls + ): + if host.backend.get_hostname() != "borg-client-2": + return + + result, calls = run_script_with_fake_borg( + host, + "/usr/local/bin/run_borg_prune@borg-server-2", + failed_command, + failed_status, + ) + + assert result.rc == failed_status + assert calls == expected_calls + def test_backup_script_contains_compression(self, host): hostname = host.backend.get_hostname() @@ -206,18 +426,15 @@ class TestBackupScript: 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") - assert script.contains("lz4") - elif hostname in ("borg-client", "borg-client-nonroot"): - script = ( - host.file("/usr/local/bin/run_borg_backup") - if hostname == "borg-client" - else host.file("/usr/local/bin/run_borg_backup@borg-server") - ) + elif hostname in ( + "borg-client", + "borg-client-2", + "borg-client-nonroot", + "borg-client-transition", + ): + script = host.file(get_backup_script_paths(hostname)[0]) assert script.contains("-C") - assert script.contains("zstd") + assert script.contains("lz4" if hostname == "borg-client-2" else "zstd") else: pytest.fail(f"Unexpected hostname: {hostname}") @@ -243,13 +460,17 @@ class TestBackupScript: assert script.contains("borg@borg-server") assert script.contains("/opt/borg") elif hostname == "borg-client": - script = host.file("/usr/local/bin/run_borg_backup") + script = host.file(get_backup_script_paths(hostname)[0]) assert script.contains("borg@borg-server") assert script.contains("/opt/borg") elif hostname == "borg-client-2": - script = host.file("/usr/local/bin/run_borg_backup") + script = host.file(get_backup_script_paths(hostname)[0]) assert script.contains("backupserver@borg-server-2") assert script.contains("/var/backups") + elif hostname == "borg-client-transition": + script = host.file("/usr/local/bin/run_borg_backup@transition-repo") + assert script.contains("borg@borg-server") + assert script.contains("/opt/borg/transition-repo") else: pytest.fail(f"Unexpected hostname: {hostname}") @@ -272,8 +493,8 @@ class TestBackupScript: script = host.file("/usr/local/bin/run_borg_backup@borg-server") content = script.content_string assert "/etc" in content - elif hostname in ("borg-client", "borg-client-2"): - script = host.file("/usr/local/bin/run_borg_backup") + elif hostname in ("borg-client", "borg-client-2", "borg-client-transition"): + script = host.file(get_backup_script_paths(hostname)[0]) content = script.content_string assert "/etc" in content or "/home" in content else: @@ -295,8 +516,8 @@ class TestBackupScript: elif hostname == "borg-client-nonroot": script = host.file("/usr/local/bin/run_borg_backup@borg-server") assert script.mode == 0o711 - elif hostname in ("borg-client", "borg-client-2"): - script = host.file("/usr/local/bin/run_borg_backup") + elif hostname in ("borg-client", "borg-client-2", "borg-client-transition"): + script = host.file(get_backup_script_paths(hostname)[0]) assert script.mode == 0o711 else: pytest.fail(f"Unexpected hostname: {hostname}") @@ -313,7 +534,7 @@ class TestBackupScript: assert "--one-file-system" in script2.content_string assert "--exclude-caches" in script2.content_string elif hostname == "borg-client-2": - script = host.file("/usr/local/bin/run_borg_backup") + script = host.file(get_backup_script_paths(hostname)[0]) content = script.content_string assert "-C zlib,6" in content @@ -322,7 +543,7 @@ class TestBackupScript: if hostname != "borg-client-2": return - script = host.file("/usr/local/bin/run_borg_backup") + script = host.file(get_backup_script_paths(hostname)[0]) content = script.content_string assert "-C lz4" in content @@ -344,57 +565,15 @@ class TestBackupScript: assert "ssh -i" in script2.content_string -class TestMultiInstanceBaseScript: - def test_base_script_exists(self, host): - hostname = host.backend.get_hostname() - if hostname != "borg-client-multi": - return - - client_user = get_client_user(host) - base_script = host.file("/usr/local/bin/run_borg_backup") - assert base_script.exists - assert base_script.user == client_user - assert base_script.mode == 0o711 - - def test_base_script_contains_both_blocks(self, host): - hostname = host.backend.get_hostname() - if hostname != "borg-client-multi": - return - - base_script = host.file("/usr/local/bin/run_borg_backup") - content = base_script.content_string +class TestAggregateScripts: + def test_legacy_aggregate_backup_script_is_preserved(self, host): + aggregate = host.file("/usr/local/bin/run_borg_backup") - assert "borg-server/configs" in content - assert "borg-server/home-data" in content - - def test_base_script_contains_both_repos(self, host): - hostname = host.backend.get_hostname() - if hostname != "borg-client-multi": - return - - base_script = host.file("/usr/local/bin/run_borg_backup") - content = base_script.content_string - - assert "/opt/borg/configs" in content - assert "/opt/borg/home-data" in content - - def test_base_script_contains_both_compressions(self, host): - hostname = host.backend.get_hostname() - if hostname != "borg-client-multi": - return - - base_script = host.file("/usr/local/bin/run_borg_backup") - content = base_script.content_string - - assert "-C zstd" in content - assert "-C lz4" in content - - def test_base_script_two_borg_create_commands(self, host): - hostname = host.backend.get_hostname() - if hostname != "borg-client-multi": - return - - base_script = host.file("/usr/local/bin/run_borg_backup") - content = base_script.content_string + if host.backend.get_hostname() == "borg-client-transition": + assert aggregate.exists + assert "legacy-backup-block" in aggregate.content_string + else: + assert not aggregate.exists - assert content.count("borg create") == 2 + def test_aggregate_prune_script_is_not_created(self, host): + assert not host.file("/usr/local/bin/run_borg_prune").exists |