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,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