1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
"""Tests for borg server setup and repository configuration"""
import pytest
testinfra_hosts = ['borg-server']
class TestBorgUser:
def test_user_exists(self, host):
user = host.user('borg')
assert user.exists
assert user.home == '/opt/borg'
def test_home_exists(self, host):
home = host.file('/opt/borg')
assert home.exists
assert home.is_directory
assert home.user == 'borg'
assert home.group == 'borg'
class TestBorgSSHSetup:
def test_ssh_directory_exists(self, host):
ssh_dir = host.file('/opt/borg/.ssh')
assert ssh_dir.exists
assert ssh_dir.is_directory
assert ssh_dir.user == 'borg'
assert ssh_dir.group == 'borg'
assert ssh_dir.mode == 0o700
def test_authorized_keys_exists(self, host):
auth_keys = host.file('/opt/borg/.ssh/authorized_keys')
assert auth_keys.exists
assert auth_keys.user == 'borg'
assert auth_keys.group == 'borg'
assert not auth_keys.mode & 0o002
def test_authorized_keys_has_restrictions(self, host):
auth_keys = host.file('/opt/borg/.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')
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
configs_count = content.count('/opt/borg/configs')
home_data_count = content.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')
content = auth_keys.content_string
for line in content.split('\n'):
if not line.strip():
continue
assert line.startswith('restrict,command="borg serve'), f"Line should start with restrict,command: {line[:50]}"
assert '@' in line and line.rstrip().endswith(('borg-client', 'borg-client-2', 'borg-client-multi', 'borg-client-nonroot')), f"Line should contain user@hostname marker: {line[-40:]}"
assert '--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 and 'root@borg-client-multi' in line
]
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'
)
class TestBorgRepository:
def test_repo_directory_exists(self, host):
repo = host.file('/opt/borg/borg-client')
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
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_has_encryption(self, host):
config = host.file('/opt/borg/borg-client/config')
assert config.exists
content = 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
data = host.file('/opt/borg/borg-client/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'
|