diff options
| -rw-r--r-- | README.md | 19 | ||||
| -rw-r--r-- | defaults/main.yml | 11 | ||||
| -rw-r--r-- | meta/argument_specs.yml | 10 | ||||
| -rw-r--r-- | molecule/default/converge.yml | 54 | ||||
| -rw-r--r-- | molecule/default/molecule.yml | 8 | ||||
| -rw-r--r-- | molecule/default/tests/test_client_setup.py | 6 | ||||
| -rw-r--r-- | molecule/default/tests/test_installation.py | 2 | ||||
| -rw-r--r-- | molecule/default/tests/test_manual_backup.py | 21 | ||||
| -rw-r--r-- | molecule/default/tests/test_server_setup.py | 207 | ||||
| -rw-r--r-- | molecule/default/tests/test_server_setup_multi_instance.py | 134 | ||||
| -rw-r--r-- | molecule/default/tests/test_ssh_connectivity.py | 79 | ||||
| -rw-r--r-- | molecule/default/tests/test_systemd.py | 317 | ||||
| -rw-r--r-- | tasks/client_create_scripts_each.yml | 2 | ||||
| -rw-r--r-- | tasks/client_setup.yml | 8 | ||||
| -rw-r--r-- | tasks/server_setup.yml | 64 |
15 files changed, 530 insertions, 412 deletions
@@ -91,14 +91,25 @@ borg_server_host_ssh_key: "" # Required borg_server_host_url: { { borg_server_host } } # Optional ``` -The role creates a user for the Borg repositories on the Borg server. You can -specify the location of the home directory with `borg_server_user_home`, which -will also be the location for the backups. +By default, the role uses the `borg` user on the backup server and creates it +automatically. To use a different user, set `borg_server_user`: ```yaml -borg_server_user_home: /opt/borg +borg_server_user: backupserver +borg_server_user_home: /var/backups ``` +The `borg_server_user_home` specifies the home directory of the borg user on +the server. All client repositories will be saved in this directory. + +To disable automatic user creation (when the user is managed externally), set: + +```yaml +borg_server_user_create: false +``` + +When disabled, the user must exist before running the role. + For the backup itself, there are a few Borg parameters you can configure. Not all options that Borg offers are available in the Ansible role yet. For more configuration options, see <https://borgbackup.readthedocs.io>. diff --git a/defaults/main.yml b/defaults/main.yml index c4b481e..2d71d58 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -54,6 +54,17 @@ borg_ssh_key_per_repo: false # hostnames are globally reachable. borg_server_host_url: "{{ borg_server_host }}" +# The user that runs borg serve on the backup server. +# Defaults to 'borg'. The user must exist before running the role +# unless borg_server_user_create is true. +# Change this if your server uses a different borg user. +borg_server_user: borg + +# Whether to automatically create the borg_server_user on the server. +# When true (default), the user is created automatically. +# When false, the user must exist before running the role. +borg_server_user_create: true + # The home directory of the borg user that is created on the borg server. # All borg borg client repositories will be saved in this directory on the borg # server. e.g. /opt/borg/client1 /opt/borg/client2 diff --git a/meta/argument_specs.yml b/meta/argument_specs.yml index ef7dd4d..a6e640c 100644 --- a/meta/argument_specs.yml +++ b/meta/argument_specs.yml @@ -17,6 +17,16 @@ argument_specs: ################################################################################ # Optional ################################################################################ + borg_server_user: + type: str + required: false + default: borg + + borg_server_user_create: + type: bool + required: false + default: true + borg_client_user: type: str required: false diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index d5d118d..21d4899 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -14,35 +14,50 @@ # This would usually be set by the user globally on their ansible # repository and can be a security risk to do automatically. We will # however set the variable here in the pre_tasks since it is for testing. - - name: Start ssh + - name: Start ssh on borg-server ansible.builtin.systemd: name: sshd state: started become: true - delegate_to: "{{ borg_server_host }}" + delegate_to: borg-server - - name: Fetch ssh_key + - name: Start ssh on borg-server-2 + ansible.builtin.systemd: + name: sshd + state: started + become: true + delegate_to: borg-server-2 + + - name: Fetch ssh_key for borg-server ansible.builtin.command: >- - ssh-keyscan -t rsa {{ borg_server_host }} | sed "s/^[^ ]* //" + ssh-keyscan -t rsa borg-server | sed "s/^[^ ]* //" register: borg_server_ssh_keyscan changed_when: false - - name: Set ssh_key + - name: Fetch ssh_key for borg-server-2 + ansible.builtin.command: >- + ssh-keyscan -t rsa borg-server-2 | sed "s/^[^ ]* //" + register: borg_server_2_ssh_keyscan + changed_when: false + + - name: Set ssh_key for borg-server ansible.builtin.set_fact: borg_server_host_ssh_key: >- {{ borg_server_ssh_keyscan.stdout | split(" ") | reject("search", borg_server_host) | join(" ") }} + when: borg_server_host_ssh_key is not defined - - name: Create backup user for non-root test +- name: Setup - Create custom borg user on borg-server-2 + hosts: borg-server-2 + tasks: + - name: Create backupserver user ansible.builtin.user: - name: backupuser - home: /home/backupuser + name: backupserver + home: /var/backups shell: /bin/bash - state: present become: true - when: inventory_hostname == 'borg-client-nonroot' - name: Converge - Default borg-client hosts: borg-client @@ -61,14 +76,18 @@ - /var - /reee reeee -- name: Converge - borg-client-2 +- name: Converge - borg-client-2 (custom server user) hosts: borg-client-2 + vars: + borg_server_host_ssh_key: "{{ hostvars['localhost']['borg_server_2_ssh_keyscan']['stdout'] | split(' ') | reject('search', 'borg-server-2') | join(' ') }}" + roles: - role: kliwniloc.borgbackup vars: - borg_server_host: borg-server - borg_server_user_home: /opt/borg + borg_server_host: borg-server-2 + borg_server_user: backupserver + borg_server_user_home: /var/backups borg_decryption_keys_yaml_path: "" borg_ssh_key_type: ed25519 borg_backup_service_successful_exit_status: @@ -118,6 +137,15 @@ - name: Converge - Non-root backup user hosts: borg-client-nonroot + pre_tasks: + - name: Create backup user for non-root test + ansible.builtin.user: + name: backupuser + home: /home/backupuser + shell: /bin/bash + state: present + become: true + roles: - role: kliwniloc.borgbackup vars: diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml index 3270a63..7b8a349 100644 --- a/molecule/default/molecule.yml +++ b/molecule/default/molecule.yml @@ -59,6 +59,14 @@ platforms: networks: - name: molecule-container-net + - name: borg-server-2 + image: ${MOLECULE_DISTRO_SERVER:-debian:12} + dockerfile: Dockerfile.j2 + pre_build_image: false + privileged: true + networks: + - name: molecule-container-net + provisioner: name: ansible playbooks: diff --git a/molecule/default/tests/test_client_setup.py b/molecule/default/tests/test_client_setup.py index a0e4f4a..6d11a93 100644 --- a/molecule/default/tests/test_client_setup.py +++ b/molecule/default/tests/test_client_setup.py @@ -223,10 +223,14 @@ class TestBackupScript: script = host.file('/usr/local/bin/run_borg_backup@borg-server') assert script.contains('borg@borg-server') assert script.contains('/opt/borg') - elif hostname in ('borg-client', 'borg-client-2'): + elif hostname == 'borg-client': script = host.file('/usr/local/bin/run_borg_backup') 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') + assert script.contains('backupserver@borg-server-2') + assert script.contains('/var/backups') else: pytest.fail(f"Unexpected hostname: {hostname}") diff --git a/molecule/default/tests/test_installation.py b/molecule/default/tests/test_installation.py index b1be621..bacc320 100644 --- a/molecule/default/tests/test_installation.py +++ b/molecule/default/tests/test_installation.py @@ -1,4 +1,4 @@ -testinfra_hosts = ['borg-client', 'borg-server'] +testinfra_hosts = ['borg-client', 'borg-server', 'borg-server-2'] def test_borg_installed(host): diff --git a/molecule/default/tests/test_manual_backup.py b/molecule/default/tests/test_manual_backup.py index 31a8693..b4068bc 100644 --- a/molecule/default/tests/test_manual_backup.py +++ b/molecule/default/tests/test_manual_backup.py @@ -1,7 +1,13 @@ import pytest from datetime import datetime -testinfra_hosts = ['borg-client'] +testinfra_hosts = ['borg-client', 'borg-client-2'] + + +def get_server_info(hostname): + if hostname == 'borg-client-2': + return ('backupserver', 'borg-server-2', '/var/backups') + return ('borg', 'borg-server', '/opt/borg') compression_types = [ @@ -20,8 +26,11 @@ host""" @pytest.mark.parametrize('compression', compression_types) def test_backup_push(host, compression): + hostname = host.backend.get_hostname() + server_user, server_host, server_path = get_server_info(hostname) + c = host.run( - f'borg create -C "{compression}" borg@borg-server:/opt/borg/borg-client::testinfra-{{now:%S.%f}} /etc' + f'borg create -C "{compression}" {server_user}@{server_host}:{server_path}/{hostname}::testinfra-{{now:%S.%f}} /etc' ) assert c.rc == 0 assert c.stdout == '' @@ -30,12 +39,14 @@ def test_backup_push(host, compression): @pytest.mark.parametrize('compression', compression_types) def test_backup_restore(host, compression): + hostname = host.backend.get_hostname() + server_user, server_host, server_path = get_server_info(hostname) timestamp = datetime.now().strftime('%Y%m%d-%H%M%S-%f') archive_name = f"testinfra-backup-restore-{compression}-{timestamp}" # Create backup c = host.run( - f'borg create -C "{compression}" borg@borg-server:/opt/borg/borg-client::{archive_name} /etc' + f'borg create -C "{compression}" {server_user}@{server_host}:{server_path}/{hostname}::{archive_name} /etc' ) assert c.rc == 0 assert c.stdout == '' @@ -43,7 +54,7 @@ def test_backup_restore(host, compression): # Restore Backup c = host.run( - f"cd /mnt && borg extract borg@borg-server:/opt/borg/borg-client::{archive_name}" + f"cd /mnt && borg extract {server_user}@{server_host}:{server_path}/{hostname}::{archive_name}" ) assert c.rc == 0 assert c.stdout == '' @@ -67,7 +78,7 @@ def test_backup_restore(host, compression): assert c.stderr == '' # Delete backup - c = host.run(f"borg delete borg@borg-server:/opt/borg/borg-client::{archive_name}") + c = host.run(f"borg delete {server_user}@{server_host}:{server_path}/{hostname}::{archive_name}") assert c.rc == 0 assert c.stdout == '' assert c.stderr == '' diff --git a/molecule/default/tests/test_server_setup.py b/molecule/default/tests/test_server_setup.py index 0a987b2..590a33c 100644 --- a/molecule/default/tests/test_server_setup.py +++ b/molecule/default/tests/test_server_setup.py @@ -2,80 +2,70 @@ import pytest import re -testinfra_hosts = ['borg-server'] +testinfra_hosts = ['borg-server', 'borg-server-2'] + +SERVER_CONFIGS = { + 'borg-server': { + 'user': 'borg', + 'home': '/opt/borg', + 'client_repo': 'borg-client', + }, + 'borg-server-2': { + 'user': 'backupserver', + 'home': '/var/backups', + 'client_repo': 'borg-client-2', + }, +} + + +@pytest.fixture +def config(host): + return SERVER_CONFIGS[host.backend.hostname] class TestBorgUser: - def test_user_exists(self, host): - user = host.user('borg') + def test_user_exists(self, host, config): + user = host.user(config['user']) assert user.exists - assert user.home == '/opt/borg' + assert user.home == config['home'] - def test_home_exists(self, host): - home = host.file('/opt/borg') + def test_home_exists(self, host, config): + home = host.file(config['home']) assert home.exists assert home.is_directory - assert home.user == 'borg' - assert home.group == 'borg' + assert home.user == config['user'] + assert home.group == config['user'] class TestBorgSSHSetup: - def test_ssh_directory_exists(self, host): - ssh_dir = host.file('/opt/borg/.ssh') + def test_ssh_directory_exists(self, host, config): + ssh_dir = host.file(f"{config['home']}/.ssh") assert ssh_dir.exists assert ssh_dir.is_directory - assert ssh_dir.user == 'borg' - assert ssh_dir.group == 'borg' + assert ssh_dir.user == config['user'] + assert ssh_dir.group == config['user'] assert ssh_dir.mode == 0o700 - def test_authorized_keys_exists(self, host): - auth_keys = host.file('/opt/borg/.ssh/authorized_keys') + def test_authorized_keys_exists(self, host, config): + auth_keys = host.file(f"{config['home']}/.ssh/authorized_keys") assert auth_keys.exists - assert auth_keys.user == 'borg' - assert auth_keys.group == 'borg' + assert auth_keys.user == config['user'] + assert auth_keys.group == config['user'] assert not auth_keys.mode & 0o002 - def test_authorized_keys_has_restrictions(self, host): - auth_keys = host.file('/opt/borg/.ssh/authorized_keys') + def test_authorized_keys_has_restrictions(self, host, config): + auth_keys = host.file(f"{config['home']}/.ssh/authorized_keys") content = auth_keys.content_string assert 'restrict' in content assert 'command="borg serve' in content - def test_authorized_keys_has_repository_restrictions(self, host): - auth_keys = host.file('/opt/borg/.ssh/authorized_keys') + def test_authorized_keys_has_repository_restrictions(self, host, config): + auth_keys = host.file(f"{config['home']}/.ssh/authorized_keys") content = auth_keys.content_string assert '--restrict-to-repository' in content - def test_authorized_keys_multi_instance_single_line(self, host): - auth_keys = host.file('/opt/borg/.ssh/authorized_keys') - content = auth_keys.content_string - lines_with_both_repos = [ - line for line in content.split('\n') - if line - and '/opt/borg/configs' in line - and '/opt/borg/home-data' in line - ] - assert len(lines_with_both_repos) == 1, ( - 'Expected exactly one authorized_keys line containing both configs and home-data repos, ' - f"found {len(lines_with_both_repos)}" - ) - - def test_authorized_keys_multi_instance_repo_count(self, host): - auth_keys = host.file('/opt/borg/.ssh/authorized_keys') - content = auth_keys.content_string - - 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}" - - def test_authorized_keys_format_valid(self, host): - auth_keys = host.file('/opt/borg/.ssh/authorized_keys') + def test_authorized_keys_format_valid(self, host, config): + auth_keys = host.file(f"{config['home']}/.ssh/authorized_keys") content = auth_keys.content_string valid_hosts = ( 'borg-client', @@ -91,113 +81,34 @@ class TestBorgSSHSetup: 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): - """Verify multi-instance host doesn't have repos from other hosts in authorized_keys""" - auth_keys = host.file('/opt/borg/.ssh/authorized_keys') - content = auth_keys.content_string - - multi_lines = [ - line for line in content.split('\n') - 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)}" - ) - - multi_line = multi_lines[0] - - assert '/opt/borg/configs' in multi_line, ( - 'borg-client-multi line should contain configs repo' - ) - assert '/opt/borg/home-data' in multi_line, ( - 'borg-client-multi line should contain home-data repo' - ) - - assert '/opt/borg/borg-client' not in multi_line, ( - 'borg-client-multi should NOT have access to borg-client repo' - ) - assert '/opt/borg/borg-client-2' not in multi_line, ( - 'borg-client-multi should NOT have access to borg-client-2 repo' - ) - - def test_authorized_keys_has_storage_quota(self, host): - """Verify storage quota is set in authorized_keys for configured repos""" - auth_keys = host.file('/opt/borg/.ssh/authorized_keys') - content = auth_keys.content_string - - quotas = re.findall(r'--storage-quota (\S+)', content) - assert '10G' in quotas, '10G quota should be set for configs-keys repo' - assert '50G' in quotas, '50G quota should be set for home-data-keys repo' - - def test_authorized_keys_multi_keys_different_quotas(self, host): - """Verify per-repo keys can have different storage quotas""" - auth_keys = host.file('/opt/borg/.ssh/authorized_keys') - content = auth_keys.content_string - - multi_keys_lines = [ - line for line in content.split('\n') - if line and 'root@borg-client-multi-keys' in line - ] - - assert len(multi_keys_lines) == 2, ( - f"Should have two entries for borg-client-multi-keys, found {len(multi_keys_lines)}" - ) - - configs_line = [l for l in multi_keys_lines if 'configs-keys' in l][0] - home_data_line = [l for l in multi_keys_lines if 'home-data-keys' in l][0] - - assert '--storage-quota 10G' in configs_line, ( - 'configs-keys should have 10G quota' - ) - assert '--storage-quota 50G' in home_data_line, ( - 'home-data-keys should have 50G quota' - ) - class TestBorgRepository: - def test_repo_directory_exists(self, host): - repo = host.file('/opt/borg/borg-client') + def test_repo_directory_exists(self, host, config): + repo = host.file(f"{config['home']}/{config['client_repo']}") assert repo.exists assert repo.is_directory - assert repo.user == 'borg' - assert repo.group == 'borg' - - def test_multi_instance_repos_exist(self, host): - configs_repo = host.file('/opt/borg/configs') - home_data_repo = host.file('/opt/borg/home-data') - configs_exists = configs_repo.exists - home_data_exists = home_data_repo.exists - assert configs_exists or 'skip' or home_data_exists or True - - def test_repo_initialized(self, host): - c = host.run('borg list borg@localhost:/opt/borg/borg-client') - assert c.rc == 0 or 'does not exist' not in c.stderr + assert repo.user == config['user'] + assert repo.group == config['user'] - def test_configs_repo_accessible(self, host): - c = host.run('borg list borg@localhost:/opt/borg/configs') - if c.rc != 0 and 'does not exist' in c.stderr: - pytest.skip('configs repo not created in this test run') - - def test_home_data_repo_accessible(self, host): - c = host.run('borg list borg@localhost:/opt/borg/home-data') - if c.rc != 0 and 'does not exist' in c.stderr: - pytest.skip('home-data repo not created in this test run') + def test_repo_initialized(self, host, config): + c = host.run(f"borg list {config['user']}@localhost:{config['home']}/{config['client_repo']}") + assert c.rc == 0 or 'does not exist' not in c.stderr - def test_repo_has_encryption(self, host): - config = host.file('/opt/borg/borg-client/config') - assert config.exists - content = config.content_string + def test_repo_has_encryption(self, host, config): + repo_config = host.file(f"{config['home']}/{config['client_repo']}/config") + assert repo_config.exists + content = repo_config.content_string assert len(content) > 0 - def test_repo_config_and_data_exist(self, host): - config = host.file('/opt/borg/borg-client/config') - assert config.exists + def test_repo_config_and_data_exist(self, host, config): + repo_config = host.file(f"{config['home']}/{config['client_repo']}/config") + assert repo_config.exists - data = host.file('/opt/borg/borg-client/data') + data = host.file(f"{config['home']}/{config['client_repo']}/data") assert data.exists assert data.is_directory - def test_repo_permissions(self, host): - repo = host.file('/opt/borg/borg-client') - assert repo.user == 'borg' - assert repo.group == 'borg' + def test_repo_permissions(self, host, config): + repo = host.file(f"{config['home']}/{config['client_repo']}") + assert repo.user == config['user'] + assert repo.group == config['user'] diff --git a/molecule/default/tests/test_server_setup_multi_instance.py b/molecule/default/tests/test_server_setup_multi_instance.py new file mode 100644 index 0000000..a235e2b --- /dev/null +++ b/molecule/default/tests/test_server_setup_multi_instance.py @@ -0,0 +1,134 @@ +"""Tests for borg server multi-instance repos and storage quota features""" +import pytest +import re + +testinfra_hosts = ['borg-server'] + +SERVER_CONFIG = { + 'user': 'borg', + 'home': '/opt/borg', +} + + +@pytest.fixture +def config(host): + return SERVER_CONFIG + + +class TestBorgSSHSetupMultiInstance: + """Tests for multi-instance repo configuration""" + + def test_authorized_keys_multi_instance_single_line(self, host, config): + auth_keys = host.file(f"{config['home']}/.ssh/authorized_keys") + content = auth_keys.content_string + lines_with_both_repos = [ + line for line in content.split('\n') + if line + and f"{config['home']}/configs" in line + and f"{config['home']}/home-data" in line + ] + assert len(lines_with_both_repos) == 1, ( + 'Expected exactly one authorized_keys line containing both configs and home-data repos, ' + f"found {len(lines_with_both_repos)}" + ) + + def test_authorized_keys_multi_instance_repo_count(self, host, config): + auth_keys = host.file(f"{config['home']}/.ssh/authorized_keys") + content = auth_keys.content_string + + restricted_repos = re.findall( + r'--restrict-to-repository ([^\s"]+)', + content, + ) + configs_count = restricted_repos.count(f"{config['home']}/configs") + home_data_count = restricted_repos.count(f"{config['home']}/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}" + + def test_authorized_keys_multi_instance_no_cross_host_repos(self, host, config): + """Verify multi-instance host doesn't have repos from other hosts in authorized_keys""" + auth_keys = host.file(f"{config['home']}/.ssh/authorized_keys") + content = auth_keys.content_string + + multi_lines = [ + line for line in content.split('\n') + 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)}" + ) + + multi_line = multi_lines[0] + + assert f"{config['home']}/configs" in multi_line, ( + 'borg-client-multi line should contain configs repo' + ) + assert f"{config['home']}/home-data" in multi_line, ( + 'borg-client-multi line should contain home-data repo' + ) + + assert f"{config['home']}/borg-client" not in multi_line, ( + 'borg-client-multi should NOT have access to borg-client repo' + ) + assert f"{config['home']}/borg-client-2" not in multi_line, ( + 'borg-client-multi should NOT have access to borg-client-2 repo' + ) + + +class TestBorgSSHSetupStorageQuota: + """Tests for storage quota configuration""" + + def test_authorized_keys_has_storage_quota(self, host, config): + """Verify storage quota is set in authorized_keys for configured repos""" + auth_keys = host.file(f"{config['home']}/.ssh/authorized_keys") + content = auth_keys.content_string + + quotas = re.findall(r'--storage-quota (\S+)', content) + assert '10G' in quotas, '10G quota should be set for configs-keys repo' + assert '50G' in quotas, '50G quota should be set for home-data-keys repo' + + def test_authorized_keys_multi_keys_different_quotas(self, host, config): + """Verify per-repo keys can have different storage quotas""" + auth_keys = host.file(f"{config['home']}/.ssh/authorized_keys") + content = auth_keys.content_string + + multi_keys_lines = [ + line for line in content.split('\n') + if line and 'root@borg-client-multi-keys' in line + ] + + assert len(multi_keys_lines) == 2, ( + f"Should have two entries for borg-client-multi-keys, found {len(multi_keys_lines)}" + ) + + configs_line = [l for l in multi_keys_lines if 'configs-keys' in l][0] + home_data_line = [l for l in multi_keys_lines if 'home-data-keys' in l][0] + + assert '--storage-quota 10G' in configs_line, ( + 'configs-keys should have 10G quota' + ) + assert '--storage-quota 50G' in home_data_line, ( + 'home-data-keys should have 50G quota' + ) + + +class TestBorgRepositoryMultiInstance: + """Tests for multi-instance repo accessibility""" + + def test_multi_instance_repos_exist(self, host, config): + configs_repo = host.file(f"{config['home']}/configs") + home_data_repo = host.file(f"{config['home']}/home-data") + configs_exists = configs_repo.exists + home_data_exists = home_data_repo.exists + assert configs_exists or 'skip' or home_data_exists or True + + def test_configs_repo_accessible(self, host, config): + c = host.run(f"borg list {config['user']}@localhost:{config['home']}/configs") + if c.rc != 0 and 'does not exist' in c.stderr: + pytest.skip('configs repo not created in this test run') + + def test_home_data_repo_accessible(self, host, config): + c = host.run(f"borg list {config['user']}@localhost:{config['home']}/home-data") + if c.rc != 0 and 'does not exist' in c.stderr: + pytest.skip('home-data repo not created in this test run') diff --git a/molecule/default/tests/test_ssh_connectivity.py b/molecule/default/tests/test_ssh_connectivity.py index 0cc1897..f57b7b6 100644 --- a/molecule/default/tests/test_ssh_connectivity.py +++ b/molecule/default/tests/test_ssh_connectivity.py @@ -16,52 +16,73 @@ def get_borg_rsh(host, repo_name): ) +def get_server_user(hostname): + if hostname == 'borg-client-2': + return 'backupserver' + return 'borg' + + +def get_server_host(hostname): + if hostname == 'borg-client-2': + return 'borg-server-2' + return 'borg-server' + + def test_ssh_connection_to_server(host): - c = host.run('ssh -o BatchMode=yes -o ConnectTimeout=5 borg@borg-server echo test') + hostname = host.backend.get_hostname() + server_user = get_server_user(hostname) + server_host = get_server_host(hostname) + c = host.run(f'ssh -o BatchMode=yes -o ConnectTimeout=5 {server_user}@{server_host} echo test') assert c.rc == 0 or 'Connection refused' not in c.stderr def test_no_password_prompt_on_connect(host): hostname = host.backend.get_hostname() + server_user = get_server_user(hostname) + server_host = get_server_host(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' + f'{server_user}@{server_host} exit' ) else: c = host.run( 'ssh -o BatchMode=yes -o PreferredAuthentications=publickey ' - 'borg@borg-server exit' + f'{server_user}@{server_host} exit' ) assert c.rc == 0 def test_known_hosts_has_correct_entry(host): + hostname = host.backend.get_hostname() + server_host = get_server_host(hostname) known_hosts = host.file('/root/.ssh/known_hosts') content = known_hosts.content_string - assert 'borg-server' in content + assert server_host in content assert 'ssh-' in content def test_borg_can_connect_to_server(host): hostname = host.backend.get_hostname() + server_user = get_server_user(hostname) + server_host = get_server_host(hostname) if hostname == 'borg-client-multi': - c = host.run('borg list borg@borg-server:/opt/borg/configs') + c = host.run(f'borg list {server_user}@{server_host}:/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' + f'borg list {server_user}@{server_host}:/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') + c = host.run(f'borg list {server_user}@{server_host}:/opt/borg/borg-client') assert c.rc == 0 elif hostname == 'borg-client-2': - c = host.run('borg list borg@borg-server:/opt/borg/borg-client-2') + c = host.run(f'borg list {server_user}@{server_host}:/var/backups/borg-client-2') assert c.rc == 0, 'Should be able to connect to borg-client-2 repo' else: pytest.fail(f"Unexpected hostname: {hostname}") @@ -69,28 +90,30 @@ def test_borg_can_connect_to_server(host): def test_borg_info_works(host): hostname = host.backend.get_hostname() + server_user = get_server_user(hostname) + server_host = get_server_host(hostname) if hostname == 'borg-client-multi': - c = host.run('borg info borg@borg-server:/opt/borg/configs') + c = host.run(f'borg info {server_user}@{server_host}:/opt/borg/configs') 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') + c2 = host.run(f'borg info {server_user}@{server_host}:/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' + f'borg info {server_user}@{server_host}:/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' + f'borg info {server_user}@{server_host}:/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') + c = host.run(f'borg info {server_user}@{server_host}:/opt/borg/borg-client') assert c.rc == 0 elif hostname == 'borg-client-2': - c = host.run('borg info borg@borg-server:/opt/borg/borg-client-2') + c = host.run(f'borg info {server_user}@{server_host}:/var/backups/borg-client-2') assert c.rc == 0, 'Should be able to get info for borg-client-2 repo' else: pytest.fail(f"Unexpected hostname: {hostname}") @@ -101,40 +124,48 @@ def test_multi_instance_can_access_both_repos(host): if hostname != 'borg-client-multi': return - c1 = host.run('borg list borg@borg-server:/opt/borg/configs') + server_user = get_server_user(hostname) + server_host = get_server_host(hostname) + + c1 = host.run(f'borg list {server_user}@{server_host}:/opt/borg/configs') assert c1.rc == 0, 'borg-client-multi should access configs repo' - c2 = host.run('borg list borg@borg-server:/opt/borg/home-data') + c2 = host.run(f'borg list {server_user}@{server_host}:/opt/borg/home-data') assert c2.rc == 0, 'borg-client-multi should access home-data repo' def test_multi_instance_cannot_access_other_hosts_repos(host): - """Verify borg-client-multi cannot access repos from other hosts""" hostname = host.backend.get_hostname() if hostname != 'borg-client-multi': return - c1 = host.run('borg list borg@borg-server:/opt/borg/borg-client') + server_user = get_server_user(hostname) + server_host = get_server_host(hostname) + + c1 = host.run(f'borg list {server_user}@{server_host}:/opt/borg/borg-client') assert c1.rc != 0, ( 'borg-client-multi should NOT access borg-client repo' ) - c2 = host.run('borg list borg@borg-server:/opt/borg/borg-client-2') + c2 = host.run(f'borg list {server_user}@{server_host}:/opt/borg/borg-client-2') assert c2.rc != 0, ( 'borg-client-multi should NOT access borg-client-2 repo' ) def test_single_host_cannot_access_multi_repos(host): - """Verify single-instance hosts cannot access multi-instance repos""" hostname = host.backend.get_hostname() if hostname not in ['borg-client', 'borg-client-2']: return - c1 = host.run('borg list borg@borg-server:/opt/borg/configs') - assert c1.rc != 0, ( - f'{hostname} should NOT access configs repo (belongs to borg-client-multi)' - ) + server_user = get_server_user(hostname) + server_host = get_server_host(hostname) + + if hostname == 'borg-client': + c1 = host.run(f'borg list {server_user}@{server_host}:/opt/borg/configs') + assert c1.rc != 0, ( + f'{hostname} should NOT access configs repo (belongs to borg-client-multi)' + ) c2 = host.run('borg list borg@borg-server:/opt/borg/home-data') assert c2.rc != 0, ( diff --git a/molecule/default/tests/test_systemd.py b/molecule/default/tests/test_systemd.py index b713c27..94b82c4 100644 --- a/molecule/default/tests/test_systemd.py +++ b/molecule/default/tests/test_systemd.py @@ -4,263 +4,172 @@ import pytest testinfra_hosts = ['borg-client', 'borg-client-2', 'borg-client-multi', 'borg-client-nonroot', 'borg-client-multi-keys'] +CLIENT_CONFIGS = { + 'borg-client': { + 'user': 'root', + 'server': 'borg-server', + 'schedule': '*-*-* 02:00:00', + 'success_exit_status': False, + }, + 'borg-client-2': { + 'user': 'root', + 'server': 'borg-server-2', + 'schedule': '*-*-* 03:00:00', + 'success_exit_status': True, + }, + 'borg-client-nonroot': { + 'user': 'backupuser', + 'server': 'borg-server', + 'schedule': '*-*-* 02:00:00', + 'success_exit_status': False, + }, +} -CLIENT_USER_MAP = { - 'borg-client': 'root', - 'borg-client-2': 'root', - 'borg-client-multi': 'root', - 'borg-client-nonroot': 'backupuser', - 'borg-client-multi-keys': 'root', +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', + }, + }, } -def get_client_user(host): - hostname = host.backend.get_hostname() - return CLIENT_USER_MAP.get(hostname, 'root') +@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): - hostname = host.backend.get_hostname() - - if hostname == 'borg-client-multi': - service1 = host.file('/etc/systemd/system/borg_backup@configs.service') - service2 = host.file('/etc/systemd/system/borg_backup@home-data.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-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 - assert service.user == 'root' - assert service.group == 'root' - assert service.mode == 0o644 - elif hostname in ('borg-client', 'borg-client-2'): - service = host.file('/etc/systemd/system/borg_backup@borg-server.service') + 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: - pytest.fail(f"Unexpected hostname: {hostname}") - - def test_service_content(self, host): - hostname = host.backend.get_hostname() - - if hostname == 'borg-client-multi': - service1 = host.file('/etc/systemd/system/borg_backup@configs.service') - service2 = host.file('/etc/systemd/system/borg_backup@home-data.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-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]') - assert service.contains('[Service]') - assert service.contains('[Install]') - assert service.contains('Type=oneshot') - assert service.contains('ExecStart=/usr/local/bin/run_borg_backup') - elif hostname in ('borg-client', 'borg-client-2'): - service = host.file('/etc/systemd/system/borg_backup@borg-server.service') + 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('ExecStart=/usr/local/bin/run_borg_backup') else: - pytest.fail(f"Unexpected hostname: {hostname}") - - def test_service_user(self, host): - hostname = host.backend.get_hostname() - client_user = get_client_user(host) + 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') - if hostname == 'borg-client-multi': - service1 = host.file('/etc/systemd/system/borg_backup@configs.service') - service2 = host.file('/etc/systemd/system/borg_backup@home-data.service') - 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}') - assert service.contains(f'Group={client_user}') + 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: - pytest.fail(f"Unexpected hostname: {hostname}") + 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): - hostname = host.backend.get_hostname() - if hostname in ('borg-client-multi', 'borg-client-nonroot', 'borg-client-multi-keys'): + def test_success_exit_status(self, host, config): + if config['type'] != 'single': return - - service = host.file('/etc/systemd/system/borg_backup@borg-server.service') + service = host.file(f"/etc/systemd/system/borg_backup@{config['server']}.service") assert service.exists - if hostname == 'borg-client-2': + if config['success_exit_status']: assert service.contains('SuccessExitStatus=1 TEMPFAIL') - elif hostname == 'borg-client': - assert not service.contains('SuccessExitStatus=') else: - pytest.fail(f"Unexpected hostname: {hostname}") - - @pytest.mark.parametrize('exit_status', [1, 'TEMPFAIL']) - def test_success_exit_status_values(self, host, exit_status): - hostname = host.backend.get_hostname() - if hostname == 'borg-client-2': - service = host.file('/etc/systemd/system/borg_backup@borg-server.service') - assert service.contains(str(exit_status)) + assert not service.contains('SuccessExitStatus=') class TestSystemdTimerFile: - def test_timer_file_exists(self, host): - hostname = host.backend.get_hostname() - - if hostname == 'borg-client-multi': - timer1 = host.file('/etc/systemd/system/borg_backup@configs.timer') - 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') + 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: - pytest.fail(f"Unexpected hostname: {hostname}") - - def test_timer_content(self, host): - hostname = host.backend.get_hostname() + for repo in config['repos']: + timer = host.file(f"/etc/systemd/system/borg_backup@{repo}.timer") + assert timer.exists - if hostname == 'borg-client-multi': - timer1 = host.file('/etc/systemd/system/borg_backup@configs.timer') - timer2 = host.file('/etc/systemd/system/borg_backup@home-data.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 == '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') + 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: - pytest.fail(f"Unexpected hostname: {hostname}") - - def test_timer_schedule(self, host): - hostname = host.backend.get_hostname() + 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=') - if hostname == 'borg-client-multi': - timer1 = host.file('/etc/systemd/system/borg_backup@configs.timer') - 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 - elif hostname in ('borg-client', 'borg-client-nonroot'): - timer = host.file('/etc/systemd/system/borg_backup@borg-server.timer') - assert 'OnCalendar=*-*-* 02:00:00' in timer.content_string + 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: - pytest.fail(f"Unexpected hostname: {hostname}") + 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): - hostname = host.backend.get_hostname() - - if hostname == 'borg-client-multi': - c1 = host.run('systemctl is-enabled borg_backup@configs.timer') - c2 = host.run('systemctl is-enabled borg_backup@home-data.timer') - assert c1.rc == 0 - 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' + 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: - pytest.fail(f"Unexpected hostname: {hostname}") - - def test_timer_active(self, host): - hostname = host.backend.get_hostname() - - if hostname == 'borg-client-multi': - c1 = host.run('systemctl is-active borg_backup@configs.timer') - 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' + 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: - pytest.fail(f"Unexpected hostname: {hostname}") + 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): + def test_daemon_reload_ok(self, host, config): c = host.run('systemctl daemon-reload') assert c.rc == 0 diff --git a/tasks/client_create_scripts_each.yml b/tasks/client_create_scripts_each.yml index a9c5c31..e185013 100644 --- a/tasks/client_create_scripts_each.yml +++ b/tasks/client_create_scripts_each.yml @@ -28,7 +28,7 @@ export BORG_RSH="ssh -i {{ borg_ssh_key_path }}" {% endif %} borg create -C {{ borg_compression }} \ - borg@{{ borg_server_host_url }}:{{ borg_server_user_home }}/{{ borg_repo_name }}::{{ borg_backup_name_format }} \ + {{ borg_server_user }}@{{ borg_server_host_url }}:{{ borg_server_user_home }}/{{ borg_repo_name }}::{{ borg_backup_name_format }} \ {{ borg_included_dirs | map('quote') | join(' ') }} \ {% for e in (borg_excluded_dirs | map('quote')) %} --exclude {{ e }} {% endfor %} become: true diff --git a/tasks/client_setup.yml b/tasks/client_setup.yml index 71027c3..fc32e03 100644 --- a/tasks/client_setup.yml +++ b/tasks/client_setup.yml @@ -77,8 +77,8 @@ ansible.builtin.file: path: "{{ borg_server_user_home }}/.ssh/authorized_keys" state: touch - owner: borg - group: borg + owner: "{{ borg_server_user }}" + group: "{{ borg_server_user }}" mode: "0600" access_time: preserve modification_time: preserve @@ -161,7 +161,7 @@ - name: Initialise Borg repository ansible.builtin.command: > borg init --encryption=repokey - borg@{{ borg_server_host_url }}:{{ borg_server_user_home }}/{{ borg_repo_name }} + {{ borg_server_user }}@{{ borg_server_host_url }}:{{ borg_server_user_home }}/{{ borg_repo_name }} environment: BORG_PASSPHRASE: "{{ borg_passphrase }}" BORG_RSH: "{{ ('ssh -i ' ~ borg_ssh_key_path) if borg_ssh_key_per_repo else omit }}" @@ -202,7 +202,7 @@ - name: If host new read encryption keys ansible.builtin.command: > borg key export --paper - borg@{{ borg_server_host_url }}:{{ borg_server_user_home }}/{{ borg_repo_name }} + {{ borg_server_user }}@{{ borg_server_host_url }}:{{ borg_server_user_home }}/{{ borg_repo_name }} environment: BORG_RSH: "{{ ('ssh -i ' ~ borg_ssh_key_path) if borg_ssh_key_per_repo else omit }}" become: true diff --git a/tasks/server_setup.yml b/tasks/server_setup.yml index e47d012..66b92bd 100644 --- a/tasks/server_setup.yml +++ b/tasks/server_setup.yml @@ -1,10 +1,60 @@ --- -- name: Create borg user - ansible.builtin.user: - name: borg - comment: Borgbackup user - create_home: true - home: "{{ borg_server_user_home }}" - generate_ssh_key: true +- name: Create borg_server_user on server + when: borg_server_user_create + block: + - name: Create borg user + ansible.builtin.user: + name: "{{ borg_server_user }}" + comment: Borgbackup user + create_home: true + home: "{{ borg_server_user_home }}" + generate_ssh_key: false + become: true + delegate_to: "{{ borg_server_host }}" + +- name: Ensure borg_server_user exists + ansible.builtin.getent: + database: passwd + key: "{{ borg_server_user }}" + become: true + delegate_to: "{{ borg_server_host }}" + +- name: Fail if borg_server_user does not exist + ansible.builtin.fail: + msg: | + User {{ borg_server_user }} does not exist on {{ borg_server_host }}. + Please create the user before running this role or set borg_server_user_create: true. + when: getent_passwd[borg_server_user] is not defined + delegate_to: "{{ borg_server_host }}" + +- name: Ensure borg server user home directory exists + ansible.builtin.file: + path: "{{ borg_server_user_home }}" + state: directory + owner: "{{ borg_server_user }}" + group: "{{ borg_server_user }}" + mode: "0700" + become: true + delegate_to: "{{ borg_server_host }}" + +- name: Ensure .ssh directory exists + ansible.builtin.file: + path: "{{ borg_server_user_home }}/.ssh" + state: directory + owner: "{{ borg_server_user }}" + group: "{{ borg_server_user }}" + mode: "0700" + become: true + delegate_to: "{{ borg_server_host }}" + +- name: Ensure authorized_keys file exists + ansible.builtin.file: + path: "{{ borg_server_user_home }}/.ssh/authorized_keys" + state: touch + owner: "{{ borg_server_user }}" + group: "{{ borg_server_user }}" + mode: "0600" + access_time: preserve + modification_time: preserve become: true delegate_to: "{{ borg_server_host }}" |