ceph-devstack 0.2.1__tar.gz → 0.3.0__tar.gz

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 (67) hide show
  1. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/.github/workflows/ci.yml +10 -5
  2. ceph_devstack-0.2.1/.github/workflows/tox.yml → ceph_devstack-0.3.0/.github/workflows/pytest.yml +7 -11
  3. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/.github/workflows/release.yml +1 -1
  4. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/PKG-INFO +2 -2
  5. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/__init__.py +28 -3
  6. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/cli.py +1 -0
  7. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/requirements.py +4 -14
  8. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/resources/ceph/__init__.py +14 -13
  9. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/resources/ceph/containers.py +1 -1
  10. ceph_devstack-0.3.0/ceph_devstack/resources/ceph/utils.py +38 -0
  11. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack.egg-info/PKG-INFO +2 -2
  12. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack.egg-info/SOURCES.txt +3 -4
  13. ceph_devstack-0.3.0/ceph_devstack.egg-info/scm_file_list.json +55 -0
  14. ceph_devstack-0.3.0/ceph_devstack.egg-info/scm_version.json +8 -0
  15. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/pyproject.toml +6 -1
  16. ceph_devstack-0.3.0/tests/conftest.py +43 -0
  17. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/resources/ceph/test_cephdevstack_core.py +15 -50
  18. ceph_devstack-0.3.0/tests/resources/ceph/test_devstack.py +132 -0
  19. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/resources/ceph/test_env_vars.py +14 -0
  20. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/resources/ceph/test_testnode.py +1 -0
  21. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/resources/test_container.py +1 -1
  22. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/resources/test_misc.py +2 -1
  23. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/resources/test_podmanresource.py +2 -1
  24. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/test_config.py +27 -25
  25. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/test_requirements_core.py +21 -4
  26. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/uv.lock +1 -102
  27. ceph_devstack-0.2.1/ceph_devstack/resources/ceph/exceptions.py +0 -3
  28. ceph_devstack-0.2.1/ceph_devstack/resources/ceph/utils.py +0 -45
  29. ceph_devstack-0.2.1/pytest.ini +0 -3
  30. ceph_devstack-0.2.1/tests/conftest.py +0 -9
  31. ceph_devstack-0.2.1/tests/resources/ceph/test_devstack.py +0 -182
  32. ceph_devstack-0.2.1/tox.ini +0 -9
  33. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/.flake8 +0 -0
  34. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/.gitignore +0 -0
  35. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/.pre-commit-config.yaml +0 -0
  36. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/Jenkinsfile +0 -0
  37. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/LICENSE +0 -0
  38. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/README.md +0 -0
  39. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/build_selinux_module.sh +0 -0
  40. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/Dockerfile.selinux +0 -0
  41. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/ceph_devstack.pp +0 -0
  42. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/ceph_devstack.te +0 -0
  43. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/config.toml +0 -0
  44. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/exec.py +0 -0
  45. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/host.py +0 -0
  46. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/logging.conf +0 -0
  47. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/py.typed +0 -0
  48. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/resources/__init__.py +0 -0
  49. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/resources/ceph/requirements.py +0 -0
  50. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/resources/container.py +0 -0
  51. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack/resources/misc.py +0 -0
  52. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack.egg-info/dependency_links.txt +0 -0
  53. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack.egg-info/entry_points.txt +0 -0
  54. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack.egg-info/requires.txt +0 -0
  55. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ceph_devstack.egg-info/top_level.txt +0 -0
  56. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/docs/cgroup_v2.md +0 -0
  57. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/ruff.toml +0 -0
  58. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/setup.cfg +0 -0
  59. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/__init__.py +0 -0
  60. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/resources/__init__.py +0 -0
  61. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/resources/ceph/__init__.py +0 -0
  62. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/resources/ceph/fixtures/__init__.py +0 -0
  63. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/resources/ceph/fixtures/testnode-config.toml +0 -0
  64. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/resources/ceph/test_requirements_ceph.py +0 -0
  65. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/resources/ceph/test_ssh_keypair.py +0 -0
  66. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/test_deep_merge.py +0 -0
  67. {ceph_devstack-0.2.1 → ceph_devstack-0.3.0}/tests/test_parse_args.py +0 -0
@@ -10,7 +10,7 @@ jobs:
10
10
  name: Check code style
11
11
  runs-on: ubuntu-24.04
12
12
  steps:
13
- - uses: actions/checkout@v2
13
+ - uses: actions/checkout@v7
14
14
  - name: Install precommit
15
15
  run: pip install pre-commit
16
16
  - name: Check lint
@@ -26,8 +26,10 @@ jobs:
26
26
  include:
27
27
  - os: ubuntu-24.04
28
28
  python: "3.12"
29
+ - os: ubuntu-26.04
30
+ python: "3.14"
29
31
  steps:
30
- - uses: actions/checkout@v2
32
+ - uses: actions/checkout@v7
31
33
  - name: Install packages
32
34
  run: sudo apt-get update && sudo apt-get install podman golang-github-containernetworking-plugin-dnsname sqlite3 jq
33
35
  - name: Install uv
@@ -53,9 +55,12 @@ jobs:
53
55
  run: podman exec testnode_0 journalctl
54
56
  - name: Wait
55
57
  run: uv run ceph-devstack wait teuthology
56
- - name: Dump logs
58
+ - name: Dump dispatcher log
57
59
  if: success() || failure()
58
60
  run: podman logs -f teuthology
61
+ - name: Dump job log
62
+ if: success() || failure()
63
+ run: uv run ceph-devstack logs
59
64
  - name: Create archive
60
65
  if: success() || failure()
61
66
  run: |
@@ -67,7 +72,7 @@ jobs:
67
72
  sqlite3 /tmp/dev.db ".output stdout" ".mode json" "select * from jobs" | jq | tee /tmp/artifacts/jobs.json
68
73
  - name: Upload jobs.json
69
74
  if: success() || failure()
70
- uses: actions/upload-artifact@v4
75
+ uses: actions/upload-artifact@v7.0.1
71
76
  with:
72
77
  name: jobs
73
78
  path: /tmp/artifacts/jobs.json
@@ -77,7 +82,7 @@ jobs:
77
82
  tar -czf /tmp/artifacts/archive.tar ~/.local/share/ceph-devstack/archive/
78
83
  - name: Upload log archive
79
84
  if: success() || failure()
80
- uses: actions/upload-artifact@v4
85
+ uses: actions/upload-artifact@v7.0.1
81
86
  with:
82
87
  name: archive
83
88
  path: /tmp/artifacts/archive.tar
@@ -1,4 +1,4 @@
1
- name: tox
1
+ name: pytest
2
2
 
3
3
  on:
4
4
  pull_request:
@@ -8,26 +8,22 @@ on:
8
8
 
9
9
  jobs:
10
10
  test:
11
- name: tox on python${{ matrix.python }} via ${{ matrix.os }}
11
+ name: pytest on python${{ matrix.python }} via ${{ matrix.os }}
12
12
  runs-on: ${{ matrix.os }}
13
13
  strategy:
14
14
  matrix:
15
15
  include:
16
- - os: ubuntu-22.04
17
- python: "3.10"
18
- - os: ubuntu-22.04
19
- python: "3.11"
20
16
  - os: ubuntu-24.04
21
17
  python: "3.12"
22
- - os: ubuntu-24.04
23
- python: "3.13"
18
+ - os: ubuntu-26.04
19
+ python: "3.14"
24
20
  steps:
25
- - uses: actions/checkout@v4
21
+ - uses: actions/checkout@v7
26
22
  - name: Setup Python
27
- uses: actions/setup-python@v5
23
+ uses: actions/setup-python@v6.3.0
28
24
  with:
29
25
  python-version: ${{ matrix.python }}
30
26
  - name: Install uv
31
27
  uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
32
28
  - name: Run unit tests
33
- run: uv run --extra test tox
29
+ run: uv run --all-extras pytest
@@ -15,7 +15,7 @@ jobs:
15
15
  contents: read
16
16
  steps:
17
17
  - name: Checkout
18
- uses: actions/checkout@v6
18
+ uses: actions/checkout@v7
19
19
  - name: Install uv
20
20
  uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
21
21
  - name: Install Python 3.13
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ceph-devstack
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Run a full teuthology lab on your laptop!
5
5
  Author-email: Zack Cerza <zack@cerza.org>
6
6
  License-Expression: MIT
@@ -11,7 +11,7 @@ Classifier: Natural Language :: English
11
11
  Classifier: Operating System :: POSIX :: Linux
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3 :: Only
14
- Requires-Python: >=3.10
14
+ Requires-Python: >=3.12
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: packaging
@@ -50,6 +50,8 @@ def parse_args(args: List[str]) -> argparse.Namespace:
50
50
  parser_config_set = subparsers_config.add_parser("set")
51
51
  parser_config_set.add_argument("name")
52
52
  parser_config_set.add_argument("value")
53
+ parser_config_unset = subparsers_config.add_parser("unset")
54
+ parser_config_unset.add_argument("name")
53
55
  parser_doc = subparsers.add_parser(
54
56
  "doctor", help="Check that the system meets requirements"
55
57
  )
@@ -129,6 +131,8 @@ class Config(dict):
129
131
  __slots__ = ["user_obj", "user_path"]
130
132
 
131
133
  def load(self, config_path: Path | None = None):
134
+ args = self.get("args")
135
+ self.clear()
132
136
  parsed = tomlkit.parse((Path(__file__).parent / "config.toml").read_text())
133
137
  self.update(parsed)
134
138
  if config_path:
@@ -140,6 +144,8 @@ class Config(dict):
140
144
  raise OSError(f"Config file at {self.user_path} not found!")
141
145
  else:
142
146
  self.user_obj = {}
147
+ if args:
148
+ self["args"] = args
143
149
 
144
150
  def dump(self):
145
151
  return tomlkit.dumps(self)
@@ -153,14 +159,14 @@ class Config(dict):
153
159
  try:
154
160
  obj = obj[sub_path]
155
161
  except KeyError:
156
- logger.error(f"{name} not found in config")
157
- raise
162
+ logger.debug(f"{name} not found in config")
163
+ return ""
158
164
  i += 1
159
165
  if isinstance(obj, (str, int, bool)):
160
166
  return str(obj)
161
167
  return tomlkit.dumps(obj).strip()
162
168
 
163
- def set_value(self, name: str, value: str) -> None:
169
+ def set_value(self, name: str, value: str) -> str:
164
170
  path = name.split(".")
165
171
  obj = self.user_obj
166
172
  i = 0
@@ -181,6 +187,25 @@ class Config(dict):
181
187
  self.user_path.parent.mkdir(exist_ok=True)
182
188
  self.user_path.write_text(tomlkit.dumps(self.user_obj).strip())
183
189
  i += 1
190
+ return str(item)
191
+
192
+ def unset_value(self, name: str) -> None:
193
+ path = name.split(".")
194
+ obj = self.user_obj
195
+ i = 0
196
+ last_index = len(path) - 1
197
+ while i <= last_index:
198
+ if i < last_index:
199
+ if path[i] not in obj:
200
+ break
201
+ obj = obj[path[i]]
202
+ elif i == last_index:
203
+ obj.pop(path[i])
204
+ self.update(self.user_obj)
205
+ self.user_path.parent.mkdir(exist_ok=True)
206
+ self.user_path.write_text(tomlkit.dumps(self.user_obj).strip())
207
+ i += 1
208
+ self.load(self.user_path)
184
209
 
185
210
 
186
211
  config = Config()
@@ -12,6 +12,7 @@ CONFIG_HANDLERS = {
12
12
  "dump": lambda config, args: print(config.dump()),
13
13
  "get": lambda config, args: print(config.get_value(args.name)),
14
14
  "set": lambda config, args: print(config.set_value(args.name, args.value)),
15
+ "unset": lambda config, args: config.unset_value(args.name),
15
16
  }
16
17
 
17
18
  COMMAND_HANDLERS = {
@@ -1,6 +1,5 @@
1
1
  import shlex
2
2
 
3
- from pathlib import Path
4
3
  from packaging.version import parse as parse_version, Version
5
4
  from typing import List
6
5
 
@@ -161,7 +160,9 @@ class PodmanVersion(Requirement):
161
160
  return True
162
161
 
163
162
 
164
- class PodmanRuntime(Requirement):
163
+ class PodmanRuntime(FixableRequirement):
164
+ suggest_msg = "Could not find the 'crun' container runtime"
165
+
165
166
  @property
166
167
  def fix_cmd(self):
167
168
  if self.host.os_type != "darwin":
@@ -170,19 +171,8 @@ class PodmanRuntime(Requirement):
170
171
 
171
172
  async def check(self):
172
173
  podman_info = await self.host.podman_info()
173
- storage_conf_path = podman_info["store"]["configFile"]
174
174
  runtime = podman_info["host"]["ociRuntime"]["name"]
175
- if runtime == "crun":
176
- return True
177
- else:
178
- containers_conf_path = Path(storage_conf_path).parent / "containers.conf"
179
- cmd = host.cmd(["podman", "system", "reset"])
180
- logger.error(
181
- f"The configured runtime is '{runtime}'. "
182
- f"It must be set to 'crun' in {containers_conf_path}. "
183
- f"Afterward, run '{cmd}'."
184
- )
185
- return False
175
+ return runtime == "crun"
186
176
 
187
177
 
188
178
  class SELinuxBoolean(FixableRequirement):
@@ -24,8 +24,7 @@ from ceph_devstack.resources.ceph.requirements import (
24
24
  LoopControlDeviceWriteable,
25
25
  SELinuxModule,
26
26
  )
27
- from ceph_devstack.resources.ceph.utils import get_most_recent_run, get_job_id
28
- from ceph_devstack.resources.ceph.exceptions import TooManyJobsFound
27
+ from ceph_devstack.resources.ceph.utils import get_runs, get_jobs
29
28
 
30
29
 
31
30
  class SSHKeyPair(Secret):
@@ -236,29 +235,31 @@ class CephDevStack:
236
235
  log_file = self.get_log_file(run_name, job_id)
237
236
  except FileNotFoundError:
238
237
  logger.error("No log file found")
239
- except TooManyJobsFound as e:
240
- msg = "Found too many jobs ({jobs}) for target run. Please pick a job id with -j option.".format(
241
- jobs=", ".join(e.jobs)
242
- )
243
- logger.error(msg)
244
238
  else:
245
239
  if locate:
246
- print(log_file)
240
+ print(str(log_file).replace(str(pathlib.Path.home()), "~"))
247
241
  else:
248
242
  buffer_size = 8 * 1024
249
243
  with open(log_file) as f:
250
244
  while chunk := f.read(buffer_size):
251
245
  print(chunk, end="")
252
246
 
253
- def get_log_file(self, run_name: str = "", job_id: str = ""):
254
- archive_dir = Teuthology().archive_dir.expanduser()
247
+ def get_log_file(self, run_name: str = "", job_id: str = "") -> pathlib.Path:
248
+ archive_dir = Teuthology().archive_dir
255
249
 
256
250
  if not run_name:
257
- run_name = get_most_recent_run(os.listdir(archive_dir))
258
- run_dir = archive_dir.joinpath(run_name)
251
+ runs = get_runs(archive_dir)
252
+ if not runs:
253
+ raise FileNotFoundError
254
+ run_dir = runs[0]
255
+ else:
256
+ run_dir = archive_dir.joinpath(run_name)
259
257
 
260
258
  if not job_id:
261
- job_id = get_job_id(os.listdir(run_dir))
259
+ jobs = get_jobs(run_dir)
260
+ if not jobs:
261
+ raise FileNotFoundError
262
+ job_id = jobs[0].name
262
263
 
263
264
  log_file = run_dir.joinpath(job_id, "teuthology.log")
264
265
  if not log_file.exists():
@@ -411,7 +411,7 @@ class Teuthology(Container):
411
411
  }
412
412
 
413
413
  @property
414
- def archive_dir(self):
414
+ def archive_dir(self) -> Path:
415
415
  return Path(config["data_dir"]) / "archive"
416
416
 
417
417
  async def create(self):
@@ -0,0 +1,38 @@
1
+ import pathlib
2
+ import re
3
+ from datetime import datetime
4
+ from typing import List
5
+
6
+ RUN_DIRNAME_PATTERN = re.compile(
7
+ r"^(?P<username>^[a-z_]([a-z0-9_-]{0,31}|[a-z0-9_-]{0,30}))-(?P<timestamp>\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})"
8
+ )
9
+
10
+
11
+ def get_logtimestamp(dirname: str) -> datetime:
12
+ match_ = RUN_DIRNAME_PATTERN.search(dirname)
13
+ assert match_
14
+ return datetime.strptime(match_.group("timestamp"), "%Y-%m-%d_%H:%M:%S")
15
+
16
+
17
+ def get_runs(directory: pathlib.Path) -> List[pathlib.Path]:
18
+ return sorted(
19
+ (
20
+ dir_
21
+ for dir_ in directory.expanduser().absolute().iterdir()
22
+ if RUN_DIRNAME_PATTERN.search(dir_.name)
23
+ ),
24
+ key=lambda dir_: dir_.stat().st_mtime,
25
+ reverse=True,
26
+ )
27
+
28
+
29
+ def get_jobs(directory: pathlib.Path) -> List[pathlib.Path]:
30
+ return sorted(
31
+ (
32
+ dir_
33
+ for dir_ in directory.expanduser().absolute().iterdir()
34
+ if str(dir_.name).isdigit()
35
+ ),
36
+ key=lambda dir_: dir_.stat().st_mtime,
37
+ reverse=True,
38
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ceph-devstack
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Run a full teuthology lab on your laptop!
5
5
  Author-email: Zack Cerza <zack@cerza.org>
6
6
  License-Expression: MIT
@@ -11,7 +11,7 @@ Classifier: Natural Language :: English
11
11
  Classifier: Operating System :: POSIX :: Linux
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3 :: Only
14
- Requires-Python: >=3.10
14
+ Requires-Python: >=3.12
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: packaging
@@ -6,13 +6,11 @@ LICENSE
6
6
  README.md
7
7
  build_selinux_module.sh
8
8
  pyproject.toml
9
- pytest.ini
10
9
  ruff.toml
11
- tox.ini
12
10
  uv.lock
13
11
  .github/workflows/ci.yml
12
+ .github/workflows/pytest.yml
14
13
  .github/workflows/release.yml
15
- .github/workflows/tox.yml
16
14
  ceph_devstack/Dockerfile.selinux
17
15
  ceph_devstack/__init__.py
18
16
  ceph_devstack/ceph_devstack.pp
@@ -29,13 +27,14 @@ ceph_devstack.egg-info/SOURCES.txt
29
27
  ceph_devstack.egg-info/dependency_links.txt
30
28
  ceph_devstack.egg-info/entry_points.txt
31
29
  ceph_devstack.egg-info/requires.txt
30
+ ceph_devstack.egg-info/scm_file_list.json
31
+ ceph_devstack.egg-info/scm_version.json
32
32
  ceph_devstack.egg-info/top_level.txt
33
33
  ceph_devstack/resources/__init__.py
34
34
  ceph_devstack/resources/container.py
35
35
  ceph_devstack/resources/misc.py
36
36
  ceph_devstack/resources/ceph/__init__.py
37
37
  ceph_devstack/resources/ceph/containers.py
38
- ceph_devstack/resources/ceph/exceptions.py
39
38
  ceph_devstack/resources/ceph/requirements.py
40
39
  ceph_devstack/resources/ceph/utils.py
41
40
  docs/cgroup_v2.md
@@ -0,0 +1,55 @@
1
+ {
2
+ "files": [
3
+ "Jenkinsfile",
4
+ "build_selinux_module.sh",
5
+ ".pre-commit-config.yaml",
6
+ "README.md",
7
+ "uv.lock",
8
+ "LICENSE",
9
+ "pyproject.toml",
10
+ "ruff.toml",
11
+ ".gitignore",
12
+ ".flake8",
13
+ "docs/cgroup_v2.md",
14
+ "ceph_devstack/py.typed",
15
+ "ceph_devstack/__init__.py",
16
+ "ceph_devstack/ceph_devstack.te",
17
+ "ceph_devstack/ceph_devstack.pp",
18
+ "ceph_devstack/logging.conf",
19
+ "ceph_devstack/exec.py",
20
+ "ceph_devstack/host.py",
21
+ "ceph_devstack/config.toml",
22
+ "ceph_devstack/cli.py",
23
+ "ceph_devstack/Dockerfile.selinux",
24
+ "ceph_devstack/requirements.py",
25
+ "ceph_devstack/resources/__init__.py",
26
+ "ceph_devstack/resources/container.py",
27
+ "ceph_devstack/resources/misc.py",
28
+ "ceph_devstack/resources/ceph/containers.py",
29
+ "ceph_devstack/resources/ceph/__init__.py",
30
+ "ceph_devstack/resources/ceph/utils.py",
31
+ "ceph_devstack/resources/ceph/requirements.py",
32
+ "tests/test_config.py",
33
+ "tests/__init__.py",
34
+ "tests/test_requirements_core.py",
35
+ "tests/test_deep_merge.py",
36
+ "tests/conftest.py",
37
+ "tests/test_parse_args.py",
38
+ "tests/resources/__init__.py",
39
+ "tests/resources/test_podmanresource.py",
40
+ "tests/resources/test_misc.py",
41
+ "tests/resources/test_container.py",
42
+ "tests/resources/ceph/test_env_vars.py",
43
+ "tests/resources/ceph/__init__.py",
44
+ "tests/resources/ceph/test_devstack.py",
45
+ "tests/resources/ceph/test_ssh_keypair.py",
46
+ "tests/resources/ceph/test_cephdevstack_core.py",
47
+ "tests/resources/ceph/test_testnode.py",
48
+ "tests/resources/ceph/test_requirements_ceph.py",
49
+ "tests/resources/ceph/fixtures/__init__.py",
50
+ "tests/resources/ceph/fixtures/testnode-config.toml",
51
+ ".github/workflows/release.yml",
52
+ ".github/workflows/pytest.yml",
53
+ ".github/workflows/ci.yml"
54
+ ]
55
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.3.0",
3
+ "distance": 0,
4
+ "node": "g74b5f8b0878922261edc5ba01b8937dc4bd02d7c",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-06-26"
8
+ }
@@ -12,7 +12,7 @@ classifiers = [
12
12
  ]
13
13
  keywords = ["podman", "ceph"]
14
14
  description = "Run a full teuthology lab on your laptop!"
15
- requires-python = ">=3.10"
15
+ requires-python = ">=3.12"
16
16
  dependencies = ["packaging", "pre-commit", "PyYAML", "tomlkit"]
17
17
  dynamic = ["version"]
18
18
 
@@ -45,3 +45,8 @@ requires = [
45
45
  "wheel",
46
46
  "setuptools-scm>=8",
47
47
  ]
48
+
49
+ [tool.pytest.ini_options]
50
+ testpaths = ["tests"]
51
+ asyncio_mode = "auto"
52
+ asyncio_default_fixture_loop_scope = "function"
@@ -0,0 +1,43 @@
1
+ import os
2
+ import pathlib
3
+ import pytest
4
+ import random
5
+
6
+ from datetime import datetime, timedelta
7
+
8
+ from ceph_devstack import config
9
+
10
+
11
+ @pytest.fixture(autouse=True)
12
+ def reset_config():
13
+ config.load()
14
+ yield
15
+
16
+
17
+ @pytest.fixture(scope="function")
18
+ def create_log_file():
19
+ def _create_log_file(data_dir: pathlib.Path, **kwargs) -> pathlib.Path:
20
+ parts = {
21
+ "timestamp": (datetime.now() - timedelta(days=random.randint(1, 100))),
22
+ "test_type": random.choice(["ceph", "rgw", "rbd", "mds"]),
23
+ "job_id": random.randint(1, 100),
24
+ "content": "some log data",
25
+ **kwargs,
26
+ }
27
+ timestamp = parts["timestamp"].strftime("%Y-%m-%d_%H:%M:%S")
28
+ test_type = parts["test_type"]
29
+ job_id = parts["job_id"]
30
+ content = parts["content"]
31
+
32
+ run_name = f"root-{timestamp}-orch:cephadm:{test_type}-small-main-distro-default-testnode"
33
+ log_dir = data_dir / "archive" / run_name / str(job_id)
34
+
35
+ os.makedirs(log_dir, exist_ok=True)
36
+ time_ = parts["timestamp"].timestamp()
37
+ os.utime(log_dir, times=(time_, time_))
38
+ log_file = log_dir / "teuthology.log"
39
+ log_file.write_text(content)
40
+ os.utime(log_file, times=(time_, time_))
41
+ return log_file
42
+
43
+ return _create_log_file
@@ -1,3 +1,4 @@
1
+ from datetime import datetime
1
2
  from unittest.mock import AsyncMock, MagicMock, patch
2
3
 
3
4
  import pytest
@@ -13,7 +14,6 @@ from ceph_devstack.resources.ceph.containers import (
13
14
  TestNode as _TestNode,
14
15
  Teuthology,
15
16
  )
16
- from ceph_devstack.resources.ceph.exceptions import TooManyJobsFound
17
17
 
18
18
 
19
19
  class TestCephDevStackServiceSpecs:
@@ -205,46 +205,21 @@ class TestCephDevStackGetLogFile:
205
205
  with pytest.raises(FileNotFoundError):
206
206
  devstack.get_log_file(run_name, "1")
207
207
 
208
- def test_get_log_file_uses_most_recent_when_no_run_name(self, tmp_path):
208
+ def test_get_log_file_uses_most_recent_when_no_run_name(
209
+ self, tmp_path, create_log_file
210
+ ):
211
+ config["data_dir"] = str(tmp_path)
212
+ create_log_file(
213
+ tmp_path, timestamp=datetime(year=2024, month=1, day=1), content="old log"
214
+ )
215
+ new_log_file = create_log_file(
216
+ tmp_path, timestamp=datetime(year=2025, month=1, day=1), content="new log"
217
+ )
209
218
  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
219
+ result = devstack.get_log_file("", "")
220
+ assert str(result) == str(new_log_file)
242
221
 
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(
222
+ def test_get_log_file_returns_latest_job_log_when_multiple_and_no_job_id(
248
223
  self, tmp_path
249
224
  ):
250
225
  devstack = CephDevStack()
@@ -269,17 +244,7 @@ class TestCephDevStackGetLogFile:
269
244
  mock_teuthology = MagicMock()
270
245
  mock_teuthology.archive_dir = archive_dir
271
246
  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, "")
247
+ assert devstack.get_log_file(run_name, "").parent.name == "2"
283
248
 
284
249
 
285
250
  class TestCephDevStackRemove: