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,459 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from ceph_devstack import config
|
|
6
|
+
from ceph_devstack.resources.ceph import CephDevStack
|
|
7
|
+
from ceph_devstack.resources.ceph.containers import (
|
|
8
|
+
Archive,
|
|
9
|
+
Beanstalk,
|
|
10
|
+
Paddles,
|
|
11
|
+
Postgres,
|
|
12
|
+
Pulpito,
|
|
13
|
+
TestNode as _TestNode,
|
|
14
|
+
Teuthology,
|
|
15
|
+
)
|
|
16
|
+
from ceph_devstack.resources.ceph.exceptions import TooManyJobsFound
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestCephDevStackServiceSpecs:
|
|
20
|
+
def test_service_specs_includes_all_services(self):
|
|
21
|
+
devstack = CephDevStack()
|
|
22
|
+
assert "postgres" in devstack.service_specs
|
|
23
|
+
assert "paddles" in devstack.service_specs
|
|
24
|
+
assert "beanstalk" in devstack.service_specs
|
|
25
|
+
assert "pulpito" in devstack.service_specs
|
|
26
|
+
assert "testnode" in devstack.service_specs
|
|
27
|
+
assert "teuthology" in devstack.service_specs
|
|
28
|
+
assert "archive" in devstack.service_specs
|
|
29
|
+
|
|
30
|
+
def test_service_specs_single_count_creates_single_object(self):
|
|
31
|
+
config["containers"]["postgres"]["count"] = 1
|
|
32
|
+
devstack = CephDevStack()
|
|
33
|
+
assert len(devstack.service_specs["postgres"]["objects"]) == 1
|
|
34
|
+
|
|
35
|
+
def test_service_specs_multiple_count_creates_multiple_objects(self):
|
|
36
|
+
assert config["containers"]["testnode"]["count"] == 3
|
|
37
|
+
devstack = CephDevStack()
|
|
38
|
+
assert len(devstack.service_specs["testnode"]["objects"]) == 3
|
|
39
|
+
|
|
40
|
+
def test_service_specs_zero_count_excludes_service(self):
|
|
41
|
+
config["containers"]["beanstalk"]["count"] = 0
|
|
42
|
+
devstack = CephDevStack()
|
|
43
|
+
assert "beanstalk" not in devstack.service_specs
|
|
44
|
+
|
|
45
|
+
def test_service_specs_objects_are_correct_types(self):
|
|
46
|
+
devstack = CephDevStack()
|
|
47
|
+
assert isinstance(devstack.service_specs["postgres"]["objects"][0], Postgres)
|
|
48
|
+
assert isinstance(devstack.service_specs["paddles"]["objects"][0], Paddles)
|
|
49
|
+
assert isinstance(devstack.service_specs["beanstalk"]["objects"][0], Beanstalk)
|
|
50
|
+
assert isinstance(devstack.service_specs["pulpito"]["objects"][0], Pulpito)
|
|
51
|
+
assert isinstance(devstack.service_specs["testnode"]["objects"][0], _TestNode)
|
|
52
|
+
assert isinstance(
|
|
53
|
+
devstack.service_specs["teuthology"]["objects"][0], Teuthology
|
|
54
|
+
)
|
|
55
|
+
assert isinstance(devstack.service_specs["archive"]["objects"][0], Archive)
|
|
56
|
+
|
|
57
|
+
def test_service_specs_named_objects_when_count_greater_than_one(self):
|
|
58
|
+
devstack = CephDevStack()
|
|
59
|
+
testnode_objects = devstack.service_specs["testnode"]["objects"]
|
|
60
|
+
assert testnode_objects[0].name == "testnode_0"
|
|
61
|
+
assert testnode_objects[1].name == "testnode_1"
|
|
62
|
+
assert testnode_objects[2].name == "testnode_2"
|
|
63
|
+
|
|
64
|
+
def test_service_specs_sets_postgres_paddles_url(self):
|
|
65
|
+
devstack = CephDevStack()
|
|
66
|
+
paddles_obj = devstack.service_specs["paddles"]["objects"][0]
|
|
67
|
+
assert "PADDLES_SQLALCHEMY_URL" in paddles_obj.env_vars
|
|
68
|
+
assert (
|
|
69
|
+
"postgresql+psycopg2://admin:password@postgres:5432/paddles"
|
|
70
|
+
in paddles_obj.env_vars["PADDLES_SQLALCHEMY_URL"]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def test_service_specs_does_not_set_postgres_url_when_no_postgres(self):
|
|
74
|
+
config["containers"]["postgres"]["count"] = 0
|
|
75
|
+
devstack = CephDevStack()
|
|
76
|
+
paddles_obj = devstack.service_specs["paddles"]["objects"][0]
|
|
77
|
+
assert "PADDLES_SQLALCHEMY_URL" not in paddles_obj.env_vars
|
|
78
|
+
|
|
79
|
+
def test_service_specs_count_attribute(self):
|
|
80
|
+
devstack = CephDevStack()
|
|
81
|
+
assert devstack.service_specs["postgres"]["count"] == 1
|
|
82
|
+
assert devstack.service_specs["testnode"]["count"] == 3
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestCephDevStackApply:
|
|
86
|
+
async def test_apply_calls_correct_method(self):
|
|
87
|
+
devstack = CephDevStack()
|
|
88
|
+
with patch.object(devstack, "pull", new_callable=AsyncMock) as mock_pull:
|
|
89
|
+
await devstack.apply("pull")
|
|
90
|
+
assert mock_pull.called is True
|
|
91
|
+
|
|
92
|
+
async def test_apply_calls_create(self):
|
|
93
|
+
devstack = CephDevStack()
|
|
94
|
+
with patch.object(devstack, "create", new_callable=AsyncMock) as mock_create:
|
|
95
|
+
await devstack.apply("create")
|
|
96
|
+
assert mock_create.called is True
|
|
97
|
+
|
|
98
|
+
async def test_apply_calls_start(self):
|
|
99
|
+
devstack = CephDevStack()
|
|
100
|
+
with patch.object(devstack, "start", new_callable=AsyncMock) as mock_start:
|
|
101
|
+
await devstack.apply("start")
|
|
102
|
+
assert mock_start.called is True
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TestCephDevStackPull:
|
|
106
|
+
async def test_pull_calls_pull_on_all_services(self):
|
|
107
|
+
devstack = CephDevStack()
|
|
108
|
+
# Override service_specs to control the objects
|
|
109
|
+
mock_postgres = AsyncMock()
|
|
110
|
+
mock_paddles = AsyncMock()
|
|
111
|
+
devstack.service_specs = {
|
|
112
|
+
"postgres": {"count": 1, "objects": [mock_postgres]},
|
|
113
|
+
"paddles": {"count": 1, "objects": [mock_paddles]},
|
|
114
|
+
}
|
|
115
|
+
with patch("ceph_devstack.logger.info"):
|
|
116
|
+
await devstack.pull()
|
|
117
|
+
mock_postgres.pull.assert_called_once()
|
|
118
|
+
mock_paddles.pull.assert_called_once()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestCephDevStackBuild:
|
|
122
|
+
async def test_build_calls_build_on_all_services(self):
|
|
123
|
+
devstack = CephDevStack()
|
|
124
|
+
mock_postgres = AsyncMock()
|
|
125
|
+
mock_paddles = AsyncMock()
|
|
126
|
+
devstack.service_specs = {
|
|
127
|
+
"postgres": {"count": 1, "objects": [mock_postgres]},
|
|
128
|
+
"paddles": {"count": 1, "objects": [mock_paddles]},
|
|
129
|
+
}
|
|
130
|
+
with patch("ceph_devstack.logger.info"):
|
|
131
|
+
await devstack.build()
|
|
132
|
+
mock_postgres.build.assert_called_once()
|
|
133
|
+
mock_paddles.build.assert_called_once()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestCephDevStackGetLogFile:
|
|
137
|
+
def test_get_log_file_with_run_name_and_job_id(self, tmp_path):
|
|
138
|
+
devstack = CephDevStack()
|
|
139
|
+
archive_dir = tmp_path / "archive"
|
|
140
|
+
archive_dir.mkdir()
|
|
141
|
+
run_name = "root-2025-01-01_00:00:00-orch:cephadm:smoke-small-main-distro-default-testnode"
|
|
142
|
+
run_dir = archive_dir / run_name
|
|
143
|
+
run_dir.mkdir()
|
|
144
|
+
job_dir = run_dir / "42"
|
|
145
|
+
job_dir.mkdir()
|
|
146
|
+
log_file = job_dir / "teuthology.log"
|
|
147
|
+
log_file.write_text("test log content")
|
|
148
|
+
|
|
149
|
+
# Mock Teuthology to return our test archive_dir
|
|
150
|
+
with patch("ceph_devstack.resources.ceph.Teuthology") as MockTeuthology:
|
|
151
|
+
mock_teuthology = MagicMock()
|
|
152
|
+
mock_teuthology.archive_dir = archive_dir
|
|
153
|
+
MockTeuthology.return_value = mock_teuthology
|
|
154
|
+
result = devstack.get_log_file(run_name, "42")
|
|
155
|
+
assert str(result) == str(log_file)
|
|
156
|
+
|
|
157
|
+
def test_get_log_file_with_run_name_only(self, tmp_path):
|
|
158
|
+
devstack = CephDevStack()
|
|
159
|
+
archive_dir = tmp_path / "archive"
|
|
160
|
+
archive_dir.mkdir()
|
|
161
|
+
run_name = "root-2025-01-01_00:00:00-orch:cephadm:smoke-small-main-distro-default-testnode"
|
|
162
|
+
run_dir = archive_dir / run_name
|
|
163
|
+
run_dir.mkdir()
|
|
164
|
+
job_dir = run_dir / "1"
|
|
165
|
+
job_dir.mkdir()
|
|
166
|
+
log_file = job_dir / "teuthology.log"
|
|
167
|
+
log_file.write_text("test log content")
|
|
168
|
+
|
|
169
|
+
with patch("ceph_devstack.resources.ceph.Teuthology") as MockTeuthology:
|
|
170
|
+
mock_teuthology = MagicMock()
|
|
171
|
+
mock_teuthology.archive_dir = archive_dir
|
|
172
|
+
MockTeuthology.return_value = mock_teuthology
|
|
173
|
+
result = devstack.get_log_file(run_name, "")
|
|
174
|
+
assert str(result) == str(log_file)
|
|
175
|
+
|
|
176
|
+
def test_get_log_file_raises_file_not_found_for_missing_job(self, tmp_path):
|
|
177
|
+
devstack = CephDevStack()
|
|
178
|
+
archive_dir = tmp_path / "archive"
|
|
179
|
+
archive_dir.mkdir()
|
|
180
|
+
run_name = "root-2025-01-01_00:00:00-orch:cephadm:smoke-small-main-distro-default-testnode"
|
|
181
|
+
run_dir = archive_dir / run_name
|
|
182
|
+
run_dir.mkdir()
|
|
183
|
+
|
|
184
|
+
with patch("ceph_devstack.resources.ceph.Teuthology") as MockTeuthology:
|
|
185
|
+
mock_teuthology = MagicMock()
|
|
186
|
+
mock_teuthology.archive_dir = archive_dir
|
|
187
|
+
MockTeuthology.return_value = mock_teuthology
|
|
188
|
+
with pytest.raises(FileNotFoundError):
|
|
189
|
+
devstack.get_log_file(run_name, "99")
|
|
190
|
+
|
|
191
|
+
def test_get_log_file_raises_file_not_found_for_missing_log(self, tmp_path):
|
|
192
|
+
devstack = CephDevStack()
|
|
193
|
+
archive_dir = tmp_path / "archive"
|
|
194
|
+
archive_dir.mkdir()
|
|
195
|
+
run_name = "root-2025-01-01_00:00:00-orch:cephadm:smoke-small-main-distro-default-testnode"
|
|
196
|
+
run_dir = archive_dir / run_name
|
|
197
|
+
run_dir.mkdir()
|
|
198
|
+
job_dir = run_dir / "1"
|
|
199
|
+
job_dir.mkdir()
|
|
200
|
+
|
|
201
|
+
with patch("ceph_devstack.resources.ceph.Teuthology") as MockTeuthology:
|
|
202
|
+
mock_teuthology = MagicMock()
|
|
203
|
+
mock_teuthology.archive_dir = archive_dir
|
|
204
|
+
MockTeuthology.return_value = mock_teuthology
|
|
205
|
+
with pytest.raises(FileNotFoundError):
|
|
206
|
+
devstack.get_log_file(run_name, "1")
|
|
207
|
+
|
|
208
|
+
def test_get_log_file_uses_most_recent_when_no_run_name(self, tmp_path):
|
|
209
|
+
devstack = CephDevStack()
|
|
210
|
+
archive_dir = tmp_path / "archive"
|
|
211
|
+
archive_dir.mkdir()
|
|
212
|
+
|
|
213
|
+
# Create two runs
|
|
214
|
+
older_run = "root-2024-01-01_00:00:00-orch:cephadm:smoke-small-main-distro-default-testnode"
|
|
215
|
+
newer_run = "root-2025-01-01_00:00:00-orch:cephadm:smoke-small-main-distro-default-testnode"
|
|
216
|
+
|
|
217
|
+
older_dir = archive_dir / older_run
|
|
218
|
+
older_dir.mkdir()
|
|
219
|
+
older_job = older_dir / "1"
|
|
220
|
+
older_job.mkdir()
|
|
221
|
+
(older_job / "teuthology.log").write_text("old log")
|
|
222
|
+
|
|
223
|
+
newer_dir = archive_dir / newer_run
|
|
224
|
+
newer_dir.mkdir()
|
|
225
|
+
newer_job = newer_dir / "1"
|
|
226
|
+
newer_job.mkdir()
|
|
227
|
+
log_file = newer_job / "teuthology.log"
|
|
228
|
+
log_file.write_text("new log")
|
|
229
|
+
|
|
230
|
+
# Override listdir behavior
|
|
231
|
+
def mock_listdir(path):
|
|
232
|
+
if str(path) == str(archive_dir):
|
|
233
|
+
return [older_run, newer_run]
|
|
234
|
+
if str(path) == str(newer_dir):
|
|
235
|
+
return ["1"]
|
|
236
|
+
return []
|
|
237
|
+
|
|
238
|
+
with patch("ceph_devstack.resources.ceph.Teuthology") as MockTeuthology:
|
|
239
|
+
mock_teuthology = MagicMock()
|
|
240
|
+
mock_teuthology.archive_dir = archive_dir
|
|
241
|
+
MockTeuthology.return_value = mock_teuthology
|
|
242
|
+
|
|
243
|
+
with patch("os.listdir", side_effect=mock_listdir):
|
|
244
|
+
result = devstack.get_log_file("", "")
|
|
245
|
+
assert str(result) == str(log_file)
|
|
246
|
+
|
|
247
|
+
def test_get_log_file_raises_too_many_jobs_when_multiple_and_no_job_id(
|
|
248
|
+
self, tmp_path
|
|
249
|
+
):
|
|
250
|
+
devstack = CephDevStack()
|
|
251
|
+
archive_dir = tmp_path / "archive"
|
|
252
|
+
archive_dir.mkdir()
|
|
253
|
+
|
|
254
|
+
run_name = "root-2025-01-01_00:00:00-orch:cephadm:smoke-small-main-distro-default-testnode"
|
|
255
|
+
run_dir = archive_dir / run_name
|
|
256
|
+
run_dir.mkdir()
|
|
257
|
+
|
|
258
|
+
job1_dir = run_dir / "1"
|
|
259
|
+
job1_dir.mkdir()
|
|
260
|
+
job1_log = job1_dir / "teuthology.log"
|
|
261
|
+
job1_log.write_text("job 1 log")
|
|
262
|
+
|
|
263
|
+
job2_dir = run_dir / "2"
|
|
264
|
+
job2_dir.mkdir()
|
|
265
|
+
job2_log = job2_dir / "teuthology.log"
|
|
266
|
+
job2_log.write_text("job 2 log")
|
|
267
|
+
|
|
268
|
+
with patch("ceph_devstack.resources.ceph.Teuthology") as MockTeuthology:
|
|
269
|
+
mock_teuthology = MagicMock()
|
|
270
|
+
mock_teuthology.archive_dir = archive_dir
|
|
271
|
+
MockTeuthology.return_value = mock_teuthology
|
|
272
|
+
|
|
273
|
+
def mock_listdir(path):
|
|
274
|
+
if str(path) == str(run_dir):
|
|
275
|
+
return ["1", "2"]
|
|
276
|
+
return []
|
|
277
|
+
|
|
278
|
+
with (
|
|
279
|
+
patch("os.listdir", side_effect=mock_listdir),
|
|
280
|
+
pytest.raises(TooManyJobsFound),
|
|
281
|
+
):
|
|
282
|
+
devstack.get_log_file(run_name, "")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class TestCephDevStackRemove:
|
|
286
|
+
async def test_remove_calls_remove_on_all_containers(self):
|
|
287
|
+
devstack = CephDevStack()
|
|
288
|
+
mock_postgres = AsyncMock()
|
|
289
|
+
mock_paddles = AsyncMock()
|
|
290
|
+
devstack.service_specs = {
|
|
291
|
+
"postgres": {"count": 1, "objects": [mock_postgres]},
|
|
292
|
+
"paddles": {"count": 1, "objects": [mock_paddles]},
|
|
293
|
+
}
|
|
294
|
+
with patch("ceph_devstack.resources.ceph.CephDevStackNetwork") as MockNetwork:
|
|
295
|
+
mock_network_instance = MagicMock()
|
|
296
|
+
mock_network_instance.remove = AsyncMock()
|
|
297
|
+
MockNetwork.return_value = mock_network_instance
|
|
298
|
+
with patch("ceph_devstack.resources.ceph.SSHKeyPair") as MockSecret:
|
|
299
|
+
mock_secret_instance = MagicMock()
|
|
300
|
+
mock_secret_instance.remove = AsyncMock()
|
|
301
|
+
MockSecret.return_value = mock_secret_instance
|
|
302
|
+
with patch("ceph_devstack.logger.info"):
|
|
303
|
+
await devstack.remove()
|
|
304
|
+
mock_postgres.remove.assert_called_once()
|
|
305
|
+
mock_paddles.remove.assert_called_once()
|
|
306
|
+
mock_network_instance.remove.assert_called_once()
|
|
307
|
+
mock_secret_instance.remove.assert_called_once()
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class TestCephDevStackStop:
|
|
311
|
+
async def test_stop_calls_stop_on_all_containers(self):
|
|
312
|
+
devstack = CephDevStack()
|
|
313
|
+
mock_postgres = AsyncMock()
|
|
314
|
+
mock_paddles = AsyncMock()
|
|
315
|
+
devstack.service_specs = {
|
|
316
|
+
"postgres": {"count": 1, "objects": [mock_postgres]},
|
|
317
|
+
"paddles": {"count": 1, "objects": [mock_paddles]},
|
|
318
|
+
}
|
|
319
|
+
with patch("ceph_devstack.logger.info"):
|
|
320
|
+
await devstack.stop()
|
|
321
|
+
mock_postgres.stop.assert_called_once()
|
|
322
|
+
mock_paddles.stop.assert_called_once()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class TestCephDevStackWait:
|
|
326
|
+
async def test_wait_returns_process_id(self):
|
|
327
|
+
devstack = CephDevStack()
|
|
328
|
+
mock_container = AsyncMock()
|
|
329
|
+
mock_container.name = "teuthology"
|
|
330
|
+
mock_container.wait = AsyncMock(return_value=42)
|
|
331
|
+
devstack.service_specs = {
|
|
332
|
+
"teuthology": {"count": 1, "objects": [mock_container]},
|
|
333
|
+
}
|
|
334
|
+
result = await devstack.wait("teuthology")
|
|
335
|
+
assert result == 42
|
|
336
|
+
|
|
337
|
+
async def test_wait_returns_one_for_nonexistent_container(self):
|
|
338
|
+
devstack = CephDevStack()
|
|
339
|
+
mock_container = AsyncMock()
|
|
340
|
+
mock_container.name = "teuthology"
|
|
341
|
+
devstack.service_specs = {
|
|
342
|
+
"teuthology": {"count": 1, "objects": [mock_container]},
|
|
343
|
+
}
|
|
344
|
+
result = await devstack.wait("nonexistent")
|
|
345
|
+
assert result == 1
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
class TestCephDevStackLogs:
|
|
349
|
+
async def test_logs_with_locate_true(self, tmp_path):
|
|
350
|
+
devstack = CephDevStack()
|
|
351
|
+
archive_dir = tmp_path / "archive"
|
|
352
|
+
archive_dir.mkdir()
|
|
353
|
+
run_name = "root-2025-01-01_00:00:00-orch:cephadm:smoke-small-main-distro-default-testnode"
|
|
354
|
+
run_dir = archive_dir / run_name
|
|
355
|
+
run_dir.mkdir()
|
|
356
|
+
job_dir = run_dir / "1"
|
|
357
|
+
job_dir.mkdir()
|
|
358
|
+
log_file = job_dir / "teuthology.log"
|
|
359
|
+
log_file.write_text("test log content")
|
|
360
|
+
|
|
361
|
+
import contextlib
|
|
362
|
+
import io
|
|
363
|
+
|
|
364
|
+
f = io.StringIO()
|
|
365
|
+
with patch("ceph_devstack.resources.ceph.Teuthology") as MockTeuthology:
|
|
366
|
+
mock_teuthology = MagicMock()
|
|
367
|
+
mock_teuthology.archive_dir = archive_dir
|
|
368
|
+
MockTeuthology.return_value = mock_teuthology
|
|
369
|
+
|
|
370
|
+
def mock_listdir(path):
|
|
371
|
+
if str(path) == str(archive_dir):
|
|
372
|
+
return [run_name]
|
|
373
|
+
if str(path) == str(run_dir):
|
|
374
|
+
return ["1"]
|
|
375
|
+
return []
|
|
376
|
+
|
|
377
|
+
with (
|
|
378
|
+
patch("os.listdir", side_effect=mock_listdir),
|
|
379
|
+
contextlib.redirect_stdout(f),
|
|
380
|
+
):
|
|
381
|
+
await devstack.logs(locate=True)
|
|
382
|
+
output = f.getvalue()
|
|
383
|
+
assert str(log_file) in output
|
|
384
|
+
|
|
385
|
+
async def test_logs_with_locate_false(self, tmp_path):
|
|
386
|
+
devstack = CephDevStack()
|
|
387
|
+
archive_dir = tmp_path / "archive"
|
|
388
|
+
archive_dir.mkdir()
|
|
389
|
+
run_name = "root-2025-01-01_00:00:00-orch:cephadm:smoke-small-main-distro-default-testnode"
|
|
390
|
+
run_dir = archive_dir / run_name
|
|
391
|
+
run_dir.mkdir()
|
|
392
|
+
job_dir = run_dir / "1"
|
|
393
|
+
job_dir.mkdir()
|
|
394
|
+
log_file = job_dir / "teuthology.log"
|
|
395
|
+
log_file.write_text("test log content")
|
|
396
|
+
|
|
397
|
+
import contextlib
|
|
398
|
+
import io
|
|
399
|
+
|
|
400
|
+
f = io.StringIO()
|
|
401
|
+
with patch("ceph_devstack.resources.ceph.Teuthology") as MockTeuthology:
|
|
402
|
+
mock_teuthology = MagicMock()
|
|
403
|
+
mock_teuthology.archive_dir = archive_dir
|
|
404
|
+
MockTeuthology.return_value = mock_teuthology
|
|
405
|
+
|
|
406
|
+
def mock_listdir(path):
|
|
407
|
+
if str(path) == str(archive_dir):
|
|
408
|
+
return [run_name]
|
|
409
|
+
if str(path) == str(run_dir):
|
|
410
|
+
return ["1"]
|
|
411
|
+
return []
|
|
412
|
+
|
|
413
|
+
with (
|
|
414
|
+
patch("os.listdir", side_effect=mock_listdir),
|
|
415
|
+
contextlib.redirect_stdout(f),
|
|
416
|
+
):
|
|
417
|
+
await devstack.logs(locate=False)
|
|
418
|
+
output = f.getvalue()
|
|
419
|
+
assert "test log content" in output
|
|
420
|
+
|
|
421
|
+
async def test_logs_with_missing_file_shows_error(self, tmp_path, caplog):
|
|
422
|
+
devstack = CephDevStack()
|
|
423
|
+
archive_dir = tmp_path / "archive"
|
|
424
|
+
archive_dir.mkdir()
|
|
425
|
+
run_name = "root-2025-01-01_00:00:00-orch:cephadm:smoke-small-main-distro-default-testnode"
|
|
426
|
+
run_dir = archive_dir / run_name
|
|
427
|
+
run_dir.mkdir()
|
|
428
|
+
|
|
429
|
+
with patch("ceph_devstack.resources.ceph.Teuthology") as MockTeuthology:
|
|
430
|
+
mock_teuthology = MagicMock()
|
|
431
|
+
mock_teuthology.archive_dir = archive_dir
|
|
432
|
+
MockTeuthology.return_value = mock_teuthology
|
|
433
|
+
|
|
434
|
+
def mock_listdir(path):
|
|
435
|
+
if str(path) == str(archive_dir):
|
|
436
|
+
return [run_name]
|
|
437
|
+
if str(path) == str(run_dir):
|
|
438
|
+
return ["1"]
|
|
439
|
+
return []
|
|
440
|
+
|
|
441
|
+
with patch("os.listdir", side_effect=mock_listdir):
|
|
442
|
+
await devstack.logs()
|
|
443
|
+
assert "No log file found" in caplog.text
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class TestCephDevStackInit:
|
|
447
|
+
def test_init_without_postgres(self):
|
|
448
|
+
config["containers"] = {
|
|
449
|
+
"postgres": {"image": "postgres:latest", "count": 0},
|
|
450
|
+
"paddles": {"image": "paddles:latest", "count": 1},
|
|
451
|
+
"beanstalk": {"image": "beanstalk:latest", "count": 1},
|
|
452
|
+
"pulpito": {"image": "pulpito:latest", "count": 1},
|
|
453
|
+
"testnode": {"image": "testnode:latest", "count": 3},
|
|
454
|
+
"teuthology": {"image": "teuthology:latest", "count": 1},
|
|
455
|
+
"archive": {"image": "archive:latest", "count": 1},
|
|
456
|
+
}
|
|
457
|
+
devstack = CephDevStack()
|
|
458
|
+
assert "archive" in devstack.service_specs
|
|
459
|
+
assert "postgres" not in devstack.service_specs
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import io
|
|
3
|
+
import contextlib
|
|
4
|
+
import random as rd
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
import secrets
|
|
7
|
+
import string
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from ceph_devstack import config
|
|
12
|
+
from ceph_devstack.resources.ceph.utils import (
|
|
13
|
+
get_logtimestamp,
|
|
14
|
+
get_most_recent_run,
|
|
15
|
+
get_job_id,
|
|
16
|
+
)
|
|
17
|
+
from ceph_devstack.resources.ceph.exceptions import TooManyJobsFound
|
|
18
|
+
from ceph_devstack.resources.ceph import CephDevStack
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestDevStack:
|
|
22
|
+
def test_get_logtimestamp(self):
|
|
23
|
+
dirname = "root-2025-03-20_18:34:43-orch:cephadm:smoke-small-main-distro-default-testnode"
|
|
24
|
+
assert get_logtimestamp(dirname) == datetime(2025, 3, 20, 18, 34, 43)
|
|
25
|
+
|
|
26
|
+
def test_get_most_recent_run_returns_most_recent_run(self):
|
|
27
|
+
runs = [
|
|
28
|
+
"root-2024-02-07_12:23:43-orch:cephadm:smoke-small-devlop-distro-smithi-testnode",
|
|
29
|
+
"root-2025-02-20_11:23:43-orch:cephadm:smoke-small-devlop-distro-smithi-testnode",
|
|
30
|
+
"root-2025-03-20_18:34:43-orch:cephadm:smoke-small-main-distro-default-testnode",
|
|
31
|
+
"root-2025-01-18_18:34:43-orch:cephadm:smoke-small-main-distro-default-testnode",
|
|
32
|
+
]
|
|
33
|
+
assert (
|
|
34
|
+
get_most_recent_run(runs)
|
|
35
|
+
== "root-2025-03-20_18:34:43-orch:cephadm:smoke-small-main-distro-default-testnode"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def test_get_job_id_returns_job_on_unique_job(self):
|
|
39
|
+
jobs = ["97"]
|
|
40
|
+
assert get_job_id(jobs) == "97"
|
|
41
|
+
|
|
42
|
+
def test_get_job_id_throws_filenotfound_on_missing_job(self):
|
|
43
|
+
jobs = []
|
|
44
|
+
with pytest.raises(FileNotFoundError):
|
|
45
|
+
get_job_id(jobs)
|
|
46
|
+
|
|
47
|
+
def test_get_job_id_throws_toomanyjobsfound_on_more_than_one_job(self):
|
|
48
|
+
jobs = ["1", "2"]
|
|
49
|
+
with pytest.raises(TooManyJobsFound) as exc:
|
|
50
|
+
get_job_id(jobs)
|
|
51
|
+
assert exc.value.jobs == jobs
|
|
52
|
+
|
|
53
|
+
async def test_logs_command_display_log_file_of_latest_run(
|
|
54
|
+
self, tmp_path, create_log_file
|
|
55
|
+
):
|
|
56
|
+
data_dir = str(tmp_path)
|
|
57
|
+
config["data_dir"] = data_dir
|
|
58
|
+
f = io.StringIO()
|
|
59
|
+
content = "custom log content"
|
|
60
|
+
now = datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
|
|
61
|
+
forty_days_ago = (datetime.now() - timedelta(days=40)).strftime(
|
|
62
|
+
"%Y-%m-%d_%H:%M:%S"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
create_log_file(data_dir, timestamp=now, content=content)
|
|
66
|
+
create_log_file(data_dir, timestamp=forty_days_ago)
|
|
67
|
+
|
|
68
|
+
with contextlib.redirect_stdout(f):
|
|
69
|
+
devstack = CephDevStack()
|
|
70
|
+
await devstack.logs()
|
|
71
|
+
assert content in f.getvalue()
|
|
72
|
+
|
|
73
|
+
async def test_logs_display_roughly_contents_of_log_file(
|
|
74
|
+
self, tmp_path, create_log_file
|
|
75
|
+
):
|
|
76
|
+
data_dir = str(tmp_path)
|
|
77
|
+
config["data_dir"] = data_dir
|
|
78
|
+
f = io.StringIO()
|
|
79
|
+
content = "".join(
|
|
80
|
+
secrets.choice(string.ascii_letters + string.digits)
|
|
81
|
+
for _ in range(6 * 8 * 1024)
|
|
82
|
+
)
|
|
83
|
+
now = datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
|
|
84
|
+
create_log_file(data_dir, timestamp=now, content=content)
|
|
85
|
+
|
|
86
|
+
with contextlib.redirect_stdout(f):
|
|
87
|
+
devstack = CephDevStack()
|
|
88
|
+
await devstack.logs()
|
|
89
|
+
assert content == f.getvalue()
|
|
90
|
+
|
|
91
|
+
async def test_logs_command_display_log_file_of_given_job_id(
|
|
92
|
+
self, tmp_path, create_log_file
|
|
93
|
+
):
|
|
94
|
+
data_dir = str(tmp_path)
|
|
95
|
+
config["data_dir"] = data_dir
|
|
96
|
+
f = io.StringIO()
|
|
97
|
+
content = "custom log message"
|
|
98
|
+
now = datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
|
|
99
|
+
|
|
100
|
+
create_log_file(
|
|
101
|
+
data_dir,
|
|
102
|
+
timestamp=now,
|
|
103
|
+
test_type="ceph",
|
|
104
|
+
job_id="1",
|
|
105
|
+
content="another log",
|
|
106
|
+
)
|
|
107
|
+
create_log_file(
|
|
108
|
+
data_dir, timestamp=now, test_type="ceph", job_id="2", content=content
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
with contextlib.redirect_stdout(f):
|
|
112
|
+
devstack = CephDevStack()
|
|
113
|
+
await devstack.logs(job_id="2")
|
|
114
|
+
assert content in f.getvalue()
|
|
115
|
+
|
|
116
|
+
async def test_logs_display_content_of_provided_run_name(
|
|
117
|
+
self, tmp_path, create_log_file
|
|
118
|
+
):
|
|
119
|
+
data_dir = str(tmp_path)
|
|
120
|
+
config["data_dir"] = data_dir
|
|
121
|
+
f = io.StringIO()
|
|
122
|
+
content = "custom content"
|
|
123
|
+
now = datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
|
|
124
|
+
three_days_ago = (datetime.now() - timedelta(days=3)).strftime(
|
|
125
|
+
"%Y-%m-%d_%H:%M:%S"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
create_log_file(
|
|
129
|
+
data_dir,
|
|
130
|
+
timestamp=now,
|
|
131
|
+
)
|
|
132
|
+
run_name = create_log_file(
|
|
133
|
+
data_dir,
|
|
134
|
+
timestamp=three_days_ago,
|
|
135
|
+
content=content,
|
|
136
|
+
).split("/")[-3]
|
|
137
|
+
|
|
138
|
+
with contextlib.redirect_stdout(f):
|
|
139
|
+
devstack = CephDevStack()
|
|
140
|
+
await devstack.logs(run_name=run_name)
|
|
141
|
+
assert content in f.getvalue()
|
|
142
|
+
|
|
143
|
+
async def test_logs_locate_display_file_path_instead_of_config(
|
|
144
|
+
self, tmp_path, create_log_file
|
|
145
|
+
):
|
|
146
|
+
data_dir = str(tmp_path)
|
|
147
|
+
|
|
148
|
+
config["data_dir"] = data_dir
|
|
149
|
+
f = io.StringIO()
|
|
150
|
+
log_file = create_log_file(data_dir)
|
|
151
|
+
with contextlib.redirect_stdout(f):
|
|
152
|
+
devstack = CephDevStack()
|
|
153
|
+
await devstack.logs(locate=True)
|
|
154
|
+
assert log_file in f.getvalue()
|
|
155
|
+
|
|
156
|
+
@pytest.fixture(scope="class")
|
|
157
|
+
def create_log_file(self):
|
|
158
|
+
def _create_log_file(data_dir: str, **kwargs):
|
|
159
|
+
parts = {
|
|
160
|
+
"timestamp": (
|
|
161
|
+
datetime.now() - timedelta(days=rd.randint(1, 100))
|
|
162
|
+
).strftime("%Y-%m-%d_%H:%M:%S"),
|
|
163
|
+
"test_type": rd.choice(["ceph", "rgw", "rbd", "mds"]),
|
|
164
|
+
"job_id": rd.randint(1, 100),
|
|
165
|
+
"content": "some log data",
|
|
166
|
+
**kwargs,
|
|
167
|
+
}
|
|
168
|
+
timestamp = parts["timestamp"]
|
|
169
|
+
test_type = parts["test_type"]
|
|
170
|
+
job_id = parts["job_id"]
|
|
171
|
+
content = parts["content"]
|
|
172
|
+
|
|
173
|
+
run_name = f"root-{timestamp}-orch:cephadm:{test_type}-small-main-distro-default-testnode"
|
|
174
|
+
log_dir = f"{data_dir}/archive/{run_name}/{job_id}"
|
|
175
|
+
|
|
176
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
177
|
+
log_file = f"{log_dir}/teuthology.log"
|
|
178
|
+
with open(log_file, "w") as f:
|
|
179
|
+
f.write(content)
|
|
180
|
+
return log_file
|
|
181
|
+
|
|
182
|
+
return _create_log_file
|