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.
Files changed (44) hide show
  1. ceph_devstack/Dockerfile.selinux +20 -0
  2. ceph_devstack/__init__.py +187 -0
  3. ceph_devstack/ceph_devstack.pp +0 -0
  4. ceph_devstack/ceph_devstack.te +127 -0
  5. ceph_devstack/cli.py +64 -0
  6. ceph_devstack/config.toml +24 -0
  7. ceph_devstack/exec.py +93 -0
  8. ceph_devstack/host.py +154 -0
  9. ceph_devstack/logging.conf +30 -0
  10. ceph_devstack/py.typed +0 -0
  11. ceph_devstack/requirements.py +277 -0
  12. ceph_devstack/resources/__init__.py +115 -0
  13. ceph_devstack/resources/ceph/__init__.py +266 -0
  14. ceph_devstack/resources/ceph/containers.py +419 -0
  15. ceph_devstack/resources/ceph/exceptions.py +3 -0
  16. ceph_devstack/resources/ceph/requirements.py +90 -0
  17. ceph_devstack/resources/ceph/utils.py +45 -0
  18. ceph_devstack/resources/container.py +171 -0
  19. ceph_devstack/resources/misc.py +15 -0
  20. ceph_devstack-0.1.0.dist-info/METADATA +222 -0
  21. ceph_devstack-0.1.0.dist-info/RECORD +44 -0
  22. ceph_devstack-0.1.0.dist-info/WHEEL +5 -0
  23. ceph_devstack-0.1.0.dist-info/entry_points.txt +2 -0
  24. ceph_devstack-0.1.0.dist-info/licenses/LICENSE +21 -0
  25. ceph_devstack-0.1.0.dist-info/top_level.txt +2 -0
  26. tests/__init__.py +0 -0
  27. tests/conftest.py +9 -0
  28. tests/resources/__init__.py +0 -0
  29. tests/resources/ceph/__init__.py +0 -0
  30. tests/resources/ceph/fixtures/__init__.py +0 -0
  31. tests/resources/ceph/fixtures/testnode-config.toml +2 -0
  32. tests/resources/ceph/test_cephdevstack_core.py +459 -0
  33. tests/resources/ceph/test_devstack.py +182 -0
  34. tests/resources/ceph/test_env_vars.py +110 -0
  35. tests/resources/ceph/test_requirements_ceph.py +262 -0
  36. tests/resources/ceph/test_ssh_keypair.py +109 -0
  37. tests/resources/ceph/test_testnode.py +36 -0
  38. tests/resources/test_container.py +247 -0
  39. tests/resources/test_misc.py +46 -0
  40. tests/resources/test_podmanresource.py +59 -0
  41. tests/test_config.py +120 -0
  42. tests/test_deep_merge.py +71 -0
  43. tests/test_parse_args.py +228 -0
  44. 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
+ ]