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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
|
"""Tests for borg server setup and repository configuration"""
import pytest
import re
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
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')
content = auth_keys.content_string
valid_hosts = (
'borg-client',
'borg-client-2',
'borg-client-multi',
'borg-client-nonroot',
'borg-client-multi-keys',
)
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(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')
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'
|