ceph-devstack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ceph_devstack/Dockerfile.selinux +20 -0
- ceph_devstack/__init__.py +187 -0
- ceph_devstack/ceph_devstack.pp +0 -0
- ceph_devstack/ceph_devstack.te +127 -0
- ceph_devstack/cli.py +64 -0
- ceph_devstack/config.toml +24 -0
- ceph_devstack/exec.py +93 -0
- ceph_devstack/host.py +154 -0
- ceph_devstack/logging.conf +30 -0
- ceph_devstack/py.typed +0 -0
- ceph_devstack/requirements.py +277 -0
- ceph_devstack/resources/__init__.py +115 -0
- ceph_devstack/resources/ceph/__init__.py +266 -0
- ceph_devstack/resources/ceph/containers.py +419 -0
- ceph_devstack/resources/ceph/exceptions.py +3 -0
- ceph_devstack/resources/ceph/requirements.py +90 -0
- ceph_devstack/resources/ceph/utils.py +45 -0
- ceph_devstack/resources/container.py +171 -0
- ceph_devstack/resources/misc.py +15 -0
- ceph_devstack-0.1.0.dist-info/METADATA +222 -0
- ceph_devstack-0.1.0.dist-info/RECORD +44 -0
- ceph_devstack-0.1.0.dist-info/WHEEL +5 -0
- ceph_devstack-0.1.0.dist-info/entry_points.txt +2 -0
- ceph_devstack-0.1.0.dist-info/licenses/LICENSE +21 -0
- ceph_devstack-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +9 -0
- tests/resources/__init__.py +0 -0
- tests/resources/ceph/__init__.py +0 -0
- tests/resources/ceph/fixtures/__init__.py +0 -0
- tests/resources/ceph/fixtures/testnode-config.toml +2 -0
- tests/resources/ceph/test_cephdevstack_core.py +459 -0
- tests/resources/ceph/test_devstack.py +182 -0
- tests/resources/ceph/test_env_vars.py +110 -0
- tests/resources/ceph/test_requirements_ceph.py +262 -0
- tests/resources/ceph/test_ssh_keypair.py +109 -0
- tests/resources/ceph/test_testnode.py +36 -0
- tests/resources/test_container.py +247 -0
- tests/resources/test_misc.py +46 -0
- tests/resources/test_podmanresource.py +59 -0
- tests/test_config.py +120 -0
- tests/test_deep_merge.py +71 -0
- tests/test_parse_args.py +228 -0
- tests/test_requirements_core.py +495 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ceph_devstack.resources.ceph import containers
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
ANY_VALUE = "ANY_VALUE"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _TestContainerEnvVars:
|
|
10
|
+
@pytest.fixture(scope="class")
|
|
11
|
+
def cls(self):
|
|
12
|
+
raise NotImplementedError
|
|
13
|
+
|
|
14
|
+
@pytest.fixture(scope="class")
|
|
15
|
+
def env_vars(self):
|
|
16
|
+
# return {}
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
def test_env_vars(self, cls, env_vars):
|
|
20
|
+
obj = cls()
|
|
21
|
+
if env_vars == {}:
|
|
22
|
+
assert obj.env_vars == env_vars
|
|
23
|
+
else:
|
|
24
|
+
for env_var, value in env_vars.items():
|
|
25
|
+
assert env_var in obj.env_vars
|
|
26
|
+
assert obj.env_vars[env_var] == value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestPostgres(_TestContainerEnvVars):
|
|
30
|
+
@pytest.fixture(scope="class")
|
|
31
|
+
def cls(self):
|
|
32
|
+
return containers.Postgres
|
|
33
|
+
|
|
34
|
+
@pytest.fixture(scope="class")
|
|
35
|
+
def env_vars(self):
|
|
36
|
+
return {
|
|
37
|
+
"POSTGRES_USER": "root",
|
|
38
|
+
"POSTGRES_PASSWORD": "password",
|
|
39
|
+
"APP_DB_USER": "admin",
|
|
40
|
+
"APP_DB_PASS": "password",
|
|
41
|
+
"APP_DB_NAME": "paddles",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestPaddles(_TestContainerEnvVars):
|
|
46
|
+
@pytest.fixture(scope="class")
|
|
47
|
+
def cls(self):
|
|
48
|
+
return containers.Paddles
|
|
49
|
+
|
|
50
|
+
@pytest.fixture(scope="class")
|
|
51
|
+
def env_vars(self):
|
|
52
|
+
return {
|
|
53
|
+
"PADDLES_SERVER_HOST": "0.0.0.0",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestPulpito(_TestContainerEnvVars):
|
|
58
|
+
@pytest.fixture(scope="class")
|
|
59
|
+
def cls(self):
|
|
60
|
+
return containers.Pulpito
|
|
61
|
+
|
|
62
|
+
@pytest.fixture(scope="class")
|
|
63
|
+
def env_vars(self):
|
|
64
|
+
return {
|
|
65
|
+
"PULPITO_PADDLES_ADDRESS": "http://paddles:8080",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestTestNode(_TestContainerEnvVars):
|
|
70
|
+
@pytest.fixture(scope="class")
|
|
71
|
+
def cls(self):
|
|
72
|
+
return containers.TestNode
|
|
73
|
+
|
|
74
|
+
@pytest.fixture(scope="class")
|
|
75
|
+
def env_vars(self):
|
|
76
|
+
return {
|
|
77
|
+
"CEPH_VOLUME_ALLOW_LOOP_DEVICES": "true",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestTeuthology(_TestContainerEnvVars):
|
|
82
|
+
@pytest.fixture(scope="class")
|
|
83
|
+
def cls(self):
|
|
84
|
+
return containers.Teuthology
|
|
85
|
+
|
|
86
|
+
@pytest.fixture(scope="class")
|
|
87
|
+
def env_vars(self):
|
|
88
|
+
return {
|
|
89
|
+
"SSH_PRIVKEY": "",
|
|
90
|
+
"SSH_PRIVKEY_FILE": "",
|
|
91
|
+
"TEUTHOLOGY_MACHINE_TYPE": "",
|
|
92
|
+
"TEUTHOLOGY_TESTNODES": "",
|
|
93
|
+
"TEUTHOLOGY_BRANCH": "",
|
|
94
|
+
"TEUTHOLOGY_CEPH_BRANCH": "",
|
|
95
|
+
"TEUTHOLOGY_CEPH_REPO": "",
|
|
96
|
+
"TEUTHOLOGY_SUITE": "",
|
|
97
|
+
"TEUTHOLOGY_SUITE_BRANCH": "",
|
|
98
|
+
"TEUTHOLOGY_SUITE_REPO": "",
|
|
99
|
+
"TEUTHOLOGY_SUITE_EXTRA_ARGS": "",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestBeanstalk(_TestContainerEnvVars):
|
|
104
|
+
@pytest.fixture(scope="class")
|
|
105
|
+
def cls(self):
|
|
106
|
+
return containers.Beanstalk
|
|
107
|
+
|
|
108
|
+
@pytest.fixture(scope="class")
|
|
109
|
+
def env_vars(self):
|
|
110
|
+
return {}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
2
|
+
|
|
3
|
+
from ceph_devstack import config
|
|
4
|
+
from ceph_devstack.resources.ceph import CephDevStack
|
|
5
|
+
|
|
6
|
+
from ceph_devstack.resources.ceph.requirements import (
|
|
7
|
+
HasSudo,
|
|
8
|
+
LoopControlDeviceExists,
|
|
9
|
+
LoopControlDeviceWriteable,
|
|
10
|
+
SELinuxModule,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestHasSudo:
|
|
15
|
+
def setup_method(self):
|
|
16
|
+
self.req = HasSudo()
|
|
17
|
+
|
|
18
|
+
async def test_has_sudo_check_true(self):
|
|
19
|
+
mock_proc = AsyncMock()
|
|
20
|
+
mock_proc.wait = AsyncMock(return_value=0)
|
|
21
|
+
with patch.object(self.req.host, "arun", return_value=mock_proc):
|
|
22
|
+
result = await self.req.check()
|
|
23
|
+
assert result is True
|
|
24
|
+
|
|
25
|
+
async def test_has_sudo_check_false(self):
|
|
26
|
+
mock_proc = AsyncMock()
|
|
27
|
+
mock_proc.wait = AsyncMock(return_value=1)
|
|
28
|
+
with patch.object(self.req.host, "arun", return_value=mock_proc):
|
|
29
|
+
result = await self.req.check()
|
|
30
|
+
assert result is False
|
|
31
|
+
|
|
32
|
+
def test_has_sudo_check_cmd(self):
|
|
33
|
+
assert self.req.check_cmd == ["sudo", "true"]
|
|
34
|
+
|
|
35
|
+
def test_has_sudo_suggest_msg(self):
|
|
36
|
+
assert self.req.suggest_msg == "sudo access is required"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestLoopControlDeviceExists:
|
|
40
|
+
def setup_method(self):
|
|
41
|
+
self.req = LoopControlDeviceExists()
|
|
42
|
+
|
|
43
|
+
async def test_loop_control_exists_true(self):
|
|
44
|
+
mock_proc = AsyncMock()
|
|
45
|
+
mock_proc.wait = AsyncMock(return_value=0)
|
|
46
|
+
with patch.object(self.req.host, "arun", return_value=mock_proc):
|
|
47
|
+
result = await self.req.check()
|
|
48
|
+
assert result is True
|
|
49
|
+
|
|
50
|
+
async def test_loop_control_exists_false(self):
|
|
51
|
+
mock_proc = AsyncMock()
|
|
52
|
+
mock_proc.wait = AsyncMock(return_value=1)
|
|
53
|
+
with patch.object(self.req.host, "arun", return_value=mock_proc):
|
|
54
|
+
result = await self.req.check()
|
|
55
|
+
assert result is False
|
|
56
|
+
|
|
57
|
+
def test_loop_control_exists_check_cmd(self):
|
|
58
|
+
assert self.req.check_cmd == ["test", "-e", "/dev/loop-control"]
|
|
59
|
+
|
|
60
|
+
def test_loop_control_exists_fix_cmd(self):
|
|
61
|
+
assert self.req.fix_cmd == ["sudo", "modprobe", "loop"]
|
|
62
|
+
|
|
63
|
+
def test_loop_control_exists_suggest_msg(self):
|
|
64
|
+
assert self.req.suggest_msg == "/dev/loop-control does not exist"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestLoopControlDeviceWriteable:
|
|
68
|
+
def setup_method(self):
|
|
69
|
+
self.req = LoopControlDeviceWriteable()
|
|
70
|
+
|
|
71
|
+
async def test_loop_control_writeable_check_true(self):
|
|
72
|
+
mock_proc = AsyncMock()
|
|
73
|
+
mock_proc.wait = AsyncMock(return_value=0)
|
|
74
|
+
with patch.object(self.req.host, "arun", return_value=mock_proc):
|
|
75
|
+
result = await self.req.check()
|
|
76
|
+
assert result is True
|
|
77
|
+
|
|
78
|
+
async def test_loop_control_writeable_check_false_local(self):
|
|
79
|
+
mock_check_proc = AsyncMock()
|
|
80
|
+
mock_check_proc.wait = AsyncMock(return_value=1)
|
|
81
|
+
|
|
82
|
+
mock_stat_proc = MagicMock()
|
|
83
|
+
mock_stat_proc.communicate = MagicMock(return_value=(b"disk", 0))
|
|
84
|
+
|
|
85
|
+
mock_whoami_proc = MagicMock()
|
|
86
|
+
mock_whoami_proc.communicate = MagicMock(return_value=(b"testuser", 0))
|
|
87
|
+
|
|
88
|
+
async def side_effect_arun(args):
|
|
89
|
+
if "stat" in args:
|
|
90
|
+
return mock_stat_proc
|
|
91
|
+
if "whoami" in args:
|
|
92
|
+
return mock_whoami_proc
|
|
93
|
+
return mock_check_proc
|
|
94
|
+
|
|
95
|
+
with (
|
|
96
|
+
patch.object(self.req.host, "arun", side_effect=side_effect_arun),
|
|
97
|
+
patch.object(self.req.host, "type", "local"),
|
|
98
|
+
):
|
|
99
|
+
result = await self.req.check()
|
|
100
|
+
assert result is False
|
|
101
|
+
assert "usermod" in " ".join(self.req.fix_cmd)
|
|
102
|
+
|
|
103
|
+
async def test_loop_control_writeable_check_false_remote(self):
|
|
104
|
+
mock_check_proc = AsyncMock()
|
|
105
|
+
mock_check_proc.wait = AsyncMock(return_value=1)
|
|
106
|
+
|
|
107
|
+
mock_stat_proc = MagicMock()
|
|
108
|
+
mock_stat_proc.communicate = MagicMock(return_value=(b"disk", 0))
|
|
109
|
+
|
|
110
|
+
mock_whoami_proc = MagicMock()
|
|
111
|
+
mock_whoami_proc.communicate = MagicMock(return_value=(b"testuser", 0))
|
|
112
|
+
|
|
113
|
+
async def side_effect_arun(args):
|
|
114
|
+
if "stat" in args:
|
|
115
|
+
return mock_stat_proc
|
|
116
|
+
if "whoami" in args:
|
|
117
|
+
return mock_whoami_proc
|
|
118
|
+
return mock_check_proc
|
|
119
|
+
|
|
120
|
+
with (
|
|
121
|
+
patch.object(self.req.host, "arun", side_effect=side_effect_arun),
|
|
122
|
+
patch.object(self.req.host, "type", "remote"),
|
|
123
|
+
):
|
|
124
|
+
result = await self.req.check()
|
|
125
|
+
assert result is False
|
|
126
|
+
assert "chgrp" in " ".join(self.req.fix_cmd)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TestSELinuxModule:
|
|
130
|
+
def setup_method(self):
|
|
131
|
+
self.req = SELinuxModule()
|
|
132
|
+
|
|
133
|
+
async def test_selinux_module_check_true(self):
|
|
134
|
+
mock_proc = AsyncMock()
|
|
135
|
+
mock_proc.stdout = AsyncMock()
|
|
136
|
+
mock_proc.stdout.read = AsyncMock(return_value=b"ceph_devstack\nother_module\n")
|
|
137
|
+
mock_proc.wait = AsyncMock(return_value=0)
|
|
138
|
+
with patch.object(self.req.host, "arun", return_value=mock_proc):
|
|
139
|
+
result = await self.req.check()
|
|
140
|
+
assert result is True
|
|
141
|
+
|
|
142
|
+
async def test_selinux_module_check_false(self):
|
|
143
|
+
mock_proc = AsyncMock()
|
|
144
|
+
mock_proc.stdout = AsyncMock()
|
|
145
|
+
mock_proc.stdout.read = AsyncMock(
|
|
146
|
+
return_value=b"other_module\nanother_module\n"
|
|
147
|
+
)
|
|
148
|
+
mock_proc.wait = AsyncMock(return_value=0)
|
|
149
|
+
with patch.object(self.req.host, "arun", return_value=mock_proc):
|
|
150
|
+
result = await self.req.check()
|
|
151
|
+
assert result is False
|
|
152
|
+
|
|
153
|
+
async def test_selinux_module_check_empty_output(self):
|
|
154
|
+
mock_proc = AsyncMock()
|
|
155
|
+
mock_proc.stdout = AsyncMock()
|
|
156
|
+
mock_proc.stdout.read = AsyncMock(return_value=b"")
|
|
157
|
+
mock_proc.wait = AsyncMock(return_value=0)
|
|
158
|
+
with patch.object(self.req.host, "arun", return_value=mock_proc):
|
|
159
|
+
result = await self.req.check()
|
|
160
|
+
assert result is False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class TestSELinuxModuleFixCmd:
|
|
164
|
+
def test_selinux_module_fix_cmd_local(self):
|
|
165
|
+
class MockLocalHost:
|
|
166
|
+
type = "local"
|
|
167
|
+
|
|
168
|
+
with patch.object(
|
|
169
|
+
SELinuxModule,
|
|
170
|
+
"host",
|
|
171
|
+
MockLocalHost(),
|
|
172
|
+
):
|
|
173
|
+
req = SELinuxModule()
|
|
174
|
+
assert req.fix_cmd[:3] == [
|
|
175
|
+
"sudo",
|
|
176
|
+
"semodule",
|
|
177
|
+
"-i",
|
|
178
|
+
]
|
|
179
|
+
assert req.fix_cmd[3].endswith("ceph_devstack.pp")
|
|
180
|
+
|
|
181
|
+
def test_selinux_module_fix_cmd_remote(self):
|
|
182
|
+
class MockRemoteHost:
|
|
183
|
+
type = "remote"
|
|
184
|
+
|
|
185
|
+
with patch.object(
|
|
186
|
+
SELinuxModule,
|
|
187
|
+
"host",
|
|
188
|
+
MockRemoteHost(),
|
|
189
|
+
):
|
|
190
|
+
req = SELinuxModule()
|
|
191
|
+
assert req.fix_cmd[:7] == [
|
|
192
|
+
"podman",
|
|
193
|
+
"machine",
|
|
194
|
+
"ssh",
|
|
195
|
+
"--",
|
|
196
|
+
"sudo",
|
|
197
|
+
"semodule",
|
|
198
|
+
"-i",
|
|
199
|
+
]
|
|
200
|
+
assert req.fix_cmd[7].endswith("ceph_devstack.pp")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class TestCephDevStackCheckRequirements:
|
|
204
|
+
async def test_check_requirements_returns_true_when_all_pass(self):
|
|
205
|
+
devstack = CephDevStack()
|
|
206
|
+
devstack.service_specs = {}
|
|
207
|
+
config["containers"] = {}
|
|
208
|
+
|
|
209
|
+
with (
|
|
210
|
+
patch("ceph_devstack.resources.ceph.HasSudo") as MockHasSudo,
|
|
211
|
+
patch(
|
|
212
|
+
"ceph_devstack.resources.ceph.LoopControlDeviceExists"
|
|
213
|
+
) as MockLoopCtrl,
|
|
214
|
+
patch(
|
|
215
|
+
"ceph_devstack.resources.ceph.LoopControlDeviceWriteable"
|
|
216
|
+
) as MockLoopCtrlWrite,
|
|
217
|
+
patch("ceph_devstack.host.host.selinux_enforcing") as mock_selinux,
|
|
218
|
+
):
|
|
219
|
+
mock_has_sudo = AsyncMock()
|
|
220
|
+
mock_has_sudo.evaluate = AsyncMock(return_value=True)
|
|
221
|
+
MockHasSudo.return_value = mock_has_sudo
|
|
222
|
+
mock_loop_ctrl = AsyncMock()
|
|
223
|
+
mock_loop_ctrl.evaluate = AsyncMock(return_value=True)
|
|
224
|
+
MockLoopCtrl.return_value = mock_loop_ctrl
|
|
225
|
+
mock_loop_ctrl_write = AsyncMock()
|
|
226
|
+
mock_loop_ctrl_write.evaluate = AsyncMock(return_value=True)
|
|
227
|
+
MockLoopCtrlWrite.return_value = mock_loop_ctrl_write
|
|
228
|
+
mock_selinux.return_value = False
|
|
229
|
+
result = await devstack.check_requirements()
|
|
230
|
+
assert result is True
|
|
231
|
+
|
|
232
|
+
async def test_check_requirements_returns_false_when_repo_missing(self):
|
|
233
|
+
devstack = CephDevStack()
|
|
234
|
+
devstack.service_specs = {}
|
|
235
|
+
config["containers"] = {
|
|
236
|
+
"custom": {"repo": "/nonexistent/path"},
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
with (
|
|
240
|
+
patch("ceph_devstack.resources.ceph.HasSudo") as MockHasSudo,
|
|
241
|
+
patch(
|
|
242
|
+
"ceph_devstack.resources.ceph.LoopControlDeviceExists"
|
|
243
|
+
) as MockLoopCtrl,
|
|
244
|
+
patch(
|
|
245
|
+
"ceph_devstack.resources.ceph.LoopControlDeviceWriteable"
|
|
246
|
+
) as MockLoopCtrlWrite,
|
|
247
|
+
patch("ceph_devstack.host.host.selinux_enforcing") as mock_selinux,
|
|
248
|
+
patch("ceph_devstack.host.host.path_exists") as mock_path_exists,
|
|
249
|
+
):
|
|
250
|
+
mock_has_sudo = AsyncMock()
|
|
251
|
+
mock_has_sudo.evaluate = AsyncMock(return_value=True)
|
|
252
|
+
MockHasSudo.return_value = mock_has_sudo
|
|
253
|
+
mock_loop_ctrl = AsyncMock()
|
|
254
|
+
mock_loop_ctrl.evaluate = AsyncMock(return_value=True)
|
|
255
|
+
MockLoopCtrl.return_value = mock_loop_ctrl
|
|
256
|
+
mock_loop_ctrl_write = AsyncMock()
|
|
257
|
+
mock_loop_ctrl_write.evaluate = AsyncMock(return_value=True)
|
|
258
|
+
MockLoopCtrlWrite.return_value = mock_loop_ctrl_write
|
|
259
|
+
mock_selinux.return_value = False
|
|
260
|
+
mock_path_exists.return_value = False
|
|
261
|
+
result = await devstack.check_requirements()
|
|
262
|
+
assert result is False
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import AsyncMock, patch
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from ceph_devstack.resources.ceph import SSHKeyPair
|
|
6
|
+
from tests.resources.test_misc import TestMiscResource as _TestMiscResource
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestSSHKeyPair(_TestMiscResource):
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def cls(self):
|
|
12
|
+
return SSHKeyPair
|
|
13
|
+
|
|
14
|
+
def test_name(self, cls):
|
|
15
|
+
obj = cls()
|
|
16
|
+
assert obj.name == "id_rsa"
|
|
17
|
+
|
|
18
|
+
def test_repr(self, cls):
|
|
19
|
+
obj = cls()
|
|
20
|
+
class_name = cls.__name__
|
|
21
|
+
assert repr(obj) == f'{class_name}(name="id_rsa")'
|
|
22
|
+
obj = cls(name="foo")
|
|
23
|
+
assert repr(obj) == f'{class_name}(name="foo")'
|
|
24
|
+
|
|
25
|
+
def test_ssh_key_pair_default_paths(self, cls):
|
|
26
|
+
pair = SSHKeyPair()
|
|
27
|
+
assert pair.privkey_path == "id_rsa"
|
|
28
|
+
assert pair.pubkey_path == "id_rsa.pub"
|
|
29
|
+
|
|
30
|
+
async def test_action_for_each_key(self, cls, action):
|
|
31
|
+
with patch.object(cls, "cmd"):
|
|
32
|
+
obj = cls()
|
|
33
|
+
cmds = getattr(obj, f"{action}_cmds")
|
|
34
|
+
assert len(cmds) == 2
|
|
35
|
+
assert obj.format_cmd(cmds[0])[-1] == obj.privkey_path
|
|
36
|
+
assert obj.format_cmd(cmds[1])[-1] == obj.pubkey_path
|
|
37
|
+
|
|
38
|
+
def test_ssh_key_pair_cmd_vars(self, cls):
|
|
39
|
+
obj = cls()
|
|
40
|
+
assert "name" in obj.cmd_vars
|
|
41
|
+
assert "privkey_path" in obj.cmd_vars
|
|
42
|
+
assert "pubkey_path" in obj.cmd_vars
|
|
43
|
+
|
|
44
|
+
@pytest.mark.parametrize("exists", [True, False])
|
|
45
|
+
async def test_create_when_not_exists(self, cls, exists):
|
|
46
|
+
obj = cls()
|
|
47
|
+
with (
|
|
48
|
+
patch.object(obj, "exists", return_value=exists),
|
|
49
|
+
patch.object(obj, "cmd") as mock_cmd,
|
|
50
|
+
):
|
|
51
|
+
mock_proc = AsyncMock()
|
|
52
|
+
mock_proc.wait = AsyncMock(return_value=(0 if exists else 1))
|
|
53
|
+
mock_cmd.return_value = mock_proc
|
|
54
|
+
await obj.create()
|
|
55
|
+
assert len(mock_cmd.call_args_list) == (0 if exists else 3)
|
|
56
|
+
|
|
57
|
+
async def test_ssh_key_pair_exists_both_present(self, cls):
|
|
58
|
+
obj = cls()
|
|
59
|
+
with patch.object(obj, "cmd") as mock_cmd:
|
|
60
|
+
mock_proc1 = AsyncMock()
|
|
61
|
+
mock_proc1.wait = AsyncMock(return_value=0)
|
|
62
|
+
mock_proc2 = AsyncMock()
|
|
63
|
+
mock_proc2.wait = AsyncMock(return_value=0)
|
|
64
|
+
mock_cmd.side_effect = [mock_proc1, mock_proc2]
|
|
65
|
+
result = await obj.exists()
|
|
66
|
+
assert result is True
|
|
67
|
+
assert mock_cmd.call_count == 2
|
|
68
|
+
|
|
69
|
+
async def test_ssh_key_pair_exists_first_missing(self, cls):
|
|
70
|
+
obj = cls()
|
|
71
|
+
with patch.object(obj, "cmd") as mock_cmd:
|
|
72
|
+
mock_proc1 = AsyncMock()
|
|
73
|
+
mock_proc1.wait = AsyncMock(return_value=1)
|
|
74
|
+
mock_cmd.return_value = mock_proc1
|
|
75
|
+
result = await obj.exists()
|
|
76
|
+
assert result is False
|
|
77
|
+
|
|
78
|
+
async def test_ssh_key_pair_exists_second_missing(self, cls):
|
|
79
|
+
obj = cls()
|
|
80
|
+
with patch.object(obj, "cmd") as mock_cmd:
|
|
81
|
+
mock_proc1 = AsyncMock()
|
|
82
|
+
mock_proc1.wait = AsyncMock(return_value=0)
|
|
83
|
+
mock_proc2 = AsyncMock()
|
|
84
|
+
mock_proc2.wait = AsyncMock(return_value=1)
|
|
85
|
+
mock_cmd.side_effect = [mock_proc1, mock_proc2]
|
|
86
|
+
result = await obj.exists()
|
|
87
|
+
assert result is False
|
|
88
|
+
|
|
89
|
+
async def test_ssh_key_pair_exists_when_already_exists(self, cls):
|
|
90
|
+
obj = cls()
|
|
91
|
+
with (
|
|
92
|
+
patch.object(obj, "exists") as mock_exists,
|
|
93
|
+
patch.object(obj, "_get_ssh_keys") as mock_get_keys,
|
|
94
|
+
patch.object(obj, "cmd") as mock_cmd,
|
|
95
|
+
):
|
|
96
|
+
mock_exists.return_value = True
|
|
97
|
+
await obj.create()
|
|
98
|
+
mock_exists.assert_called_once()
|
|
99
|
+
mock_get_keys.assert_not_called()
|
|
100
|
+
mock_cmd.assert_not_called()
|
|
101
|
+
|
|
102
|
+
async def test_ssh_key_pair_remove_calls_both_commands(self, cls):
|
|
103
|
+
obj = cls()
|
|
104
|
+
with patch.object(obj, "cmd") as mock_cmd:
|
|
105
|
+
mock_proc = AsyncMock()
|
|
106
|
+
mock_proc.wait = AsyncMock(return_value=0)
|
|
107
|
+
mock_cmd.return_value = mock_proc
|
|
108
|
+
await obj.remove()
|
|
109
|
+
assert mock_cmd.call_count == 2
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from ceph_devstack.resources.ceph import TestNode as _TestNode
|
|
6
|
+
from ceph_devstack import config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestTestnode:
|
|
10
|
+
@pytest.fixture(scope="class")
|
|
11
|
+
def cls(self) -> type[_TestNode]:
|
|
12
|
+
return _TestNode
|
|
13
|
+
|
|
14
|
+
def test_testnode_loop_device_count_default_to_one(self, cls):
|
|
15
|
+
testnode = cls("testnode_1")
|
|
16
|
+
assert testnode.loop_device_count == 1
|
|
17
|
+
|
|
18
|
+
def test_testnode_create_cmd_includes_related_devices(self, cls):
|
|
19
|
+
config.load(Path(__file__).parent.joinpath("fixtures", "testnode-config.toml"))
|
|
20
|
+
testnode = cls("testnode_1")
|
|
21
|
+
create_cmd = testnode.create_cmd
|
|
22
|
+
assert "--device=/dev/loop4" in create_cmd
|
|
23
|
+
assert "--device=/dev/loop5" in create_cmd
|
|
24
|
+
assert "--device=/dev/loop6" in create_cmd
|
|
25
|
+
assert "--device=/dev/loop7" in create_cmd
|
|
26
|
+
|
|
27
|
+
def test_testnode_devices_is_based_on_loop_device_count_config(self, cls):
|
|
28
|
+
config.load(Path(__file__).parent.joinpath("fixtures", "testnode-config.toml"))
|
|
29
|
+
testnode = cls("testnode_1")
|
|
30
|
+
assert testnode.loop_device_count == 4
|
|
31
|
+
assert testnode.devices == [
|
|
32
|
+
"/dev/loop4",
|
|
33
|
+
"/dev/loop5",
|
|
34
|
+
"/dev/loop6",
|
|
35
|
+
"/dev/loop7",
|
|
36
|
+
]
|