aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorColin Wilk <colin@wilk.cx>2026-06-28 22:47:11 +0200
committerColin Wilk <colin@wilk.cx>2026-06-28 23:18:35 +0200
commit7d96c1a11fc442b4efddf9ce5250cf0a9dfaf02e (patch)
tree5561dbeee61aaab05e21f3bc2895ff703a2ce137
parent785e6c88e4c0a3ccc44fd357b4f693c5aadd432c (diff)
downloadansible-role-borgbackup-7d96c1a11fc442b4efddf9ce5250cf0a9dfaf02e.tar.gz
ansible-role-borgbackup-7d96c1a11fc442b4efddf9ce5250cf0a9dfaf02e.zip
Parametrize server-side borg user
Adds option to choose a non-default borg backup user on the server.
-rw-r--r--README.md19
-rw-r--r--defaults/main.yml11
-rw-r--r--meta/argument_specs.yml10
-rw-r--r--molecule/default/converge.yml54
-rw-r--r--molecule/default/molecule.yml8
-rw-r--r--molecule/default/tests/test_client_setup.py6
-rw-r--r--molecule/default/tests/test_installation.py2
-rw-r--r--molecule/default/tests/test_manual_backup.py21
-rw-r--r--molecule/default/tests/test_server_setup.py207
-rw-r--r--molecule/default/tests/test_server_setup_multi_instance.py134
-rw-r--r--molecule/default/tests/test_ssh_connectivity.py79
-rw-r--r--molecule/default/tests/test_systemd.py317
-rw-r--r--tasks/client_create_scripts_each.yml2
-rw-r--r--tasks/client_setup.yml8
-rw-r--r--tasks/server_setup.yml64
15 files changed, 530 insertions, 412 deletions
diff --git a/README.md b/README.md
index c01e109..300a87b 100644
--- a/README.md
+++ b/README.md
@@ -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 }}"