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,419 @@
1
+ import os
2
+ import sys
3
+
4
+ from pathlib import Path
5
+ from typing import List
6
+
7
+ from ceph_devstack import config, DEFAULT_CONFIG_PATH
8
+ from ceph_devstack.host import host
9
+ from ceph_devstack.resources.container import Container
10
+
11
+
12
+ ARCHIVE_MOUNT_SUFFIX = "" if sys.platform == "darwin" else ":z"
13
+
14
+
15
+ class Postgres(Container):
16
+ create_cmd = [
17
+ "podman",
18
+ "container",
19
+ "create",
20
+ "-i",
21
+ "--network",
22
+ "ceph-devstack",
23
+ "-p",
24
+ "5432:5432",
25
+ "--health-cmd",
26
+ "CMD pg_isready -q -d paddles -U admin",
27
+ "--health-interval",
28
+ "10s",
29
+ "--health-retries",
30
+ "2",
31
+ "--health-timeout",
32
+ "5s",
33
+ "--name",
34
+ "{name}",
35
+ "{image}",
36
+ ]
37
+ env_vars = {
38
+ "POSTGRES_USER": "root",
39
+ "POSTGRES_PASSWORD": "password",
40
+ "APP_DB_USER": "admin",
41
+ "APP_DB_PASS": "password",
42
+ "APP_DB_NAME": "paddles",
43
+ }
44
+
45
+ def __init__(self, name: str = ""):
46
+ super().__init__(name)
47
+ username = self.env_vars["APP_DB_USER"]
48
+ password = self.env_vars["APP_DB_PASS"]
49
+ db_name = self.env_vars["APP_DB_NAME"]
50
+ self.paddles_sqla_url = (
51
+ f"postgresql+psycopg2://{username}:{password}@postgres:5432/{db_name}"
52
+ )
53
+
54
+
55
+ class Beanstalk(Container):
56
+ _name = "beanstalk"
57
+ create_cmd = [
58
+ "podman",
59
+ "container",
60
+ "create",
61
+ "-i",
62
+ "--network",
63
+ "ceph-devstack",
64
+ "-p",
65
+ "11300:11300",
66
+ "--name",
67
+ "{name}",
68
+ "{image}",
69
+ ]
70
+
71
+
72
+ class Paddles(Container):
73
+ create_cmd = [
74
+ "podman",
75
+ "container",
76
+ "create",
77
+ "-i",
78
+ "--network",
79
+ "ceph-devstack",
80
+ "-p",
81
+ "8080:8080",
82
+ "--health-cmd",
83
+ "CMD curl -f http://0.0.0.0:8080",
84
+ "--health-interval",
85
+ "10s",
86
+ "--health-retries",
87
+ "30",
88
+ "--health-timeout",
89
+ "5s",
90
+ "--name",
91
+ "{name}",
92
+ "{image}",
93
+ ]
94
+ env_vars = {
95
+ "PADDLES_SERVER_HOST": "0.0.0.0",
96
+ "PADDLES_JOB_LOG_HREF_TEMPL": f"http://{host.hostname()}:8000"
97
+ "/{run_name}/{job_id}/teuthology.log",
98
+ }
99
+
100
+
101
+ class Archive(Container):
102
+ cmd_vars = Container.cmd_vars + ["archive_dir"]
103
+ create_cmd = [
104
+ "podman",
105
+ "container",
106
+ "create",
107
+ "-i",
108
+ "--network",
109
+ "ceph-devstack",
110
+ "-p",
111
+ "8000:8000",
112
+ "-v",
113
+ "{archive_dir}:/archive" + ARCHIVE_MOUNT_SUFFIX,
114
+ "--name",
115
+ "{name}",
116
+ "{image}",
117
+ "python3",
118
+ "-m",
119
+ "http.server",
120
+ "-d",
121
+ "/archive",
122
+ ]
123
+
124
+ @property
125
+ def archive_dir(self):
126
+ return Path(config["data_dir"]) / "archive"
127
+
128
+
129
+ class Pulpito(Container):
130
+ create_cmd = [
131
+ "podman",
132
+ "container",
133
+ "create",
134
+ "-i",
135
+ "--network",
136
+ "ceph-devstack",
137
+ "-p",
138
+ "8081:8081",
139
+ "--health-cmd",
140
+ "CMD curl -f http://0.0.0.0:8081",
141
+ "--health-interval",
142
+ "10s",
143
+ "--health-retries",
144
+ "10",
145
+ "--health-timeout",
146
+ "5s",
147
+ "--name",
148
+ "{name}",
149
+ "{image}",
150
+ ]
151
+ env_vars = {
152
+ "PULPITO_PADDLES_ADDRESS": "http://paddles:8080",
153
+ "VITE_MACHINE_TYPE": "testnode",
154
+ }
155
+
156
+
157
+ class TestNode(Container):
158
+ _image_name = "teuthology-testnode"
159
+ capabilities = [
160
+ "SYS_ADMIN",
161
+ "NET_ADMIN",
162
+ "SYS_TIME",
163
+ "SYS_RAWIO",
164
+ "MKNOD",
165
+ "NET_RAW",
166
+ "SETUID",
167
+ "SETGID",
168
+ "CHOWN",
169
+ "SYS_PTRACE",
170
+ "SYS_TTY_CONFIG",
171
+ "AUDIT_WRITE",
172
+ "AUDIT_CONTROL",
173
+ ]
174
+ env_vars = {
175
+ "SSH_PUBKEY": "",
176
+ "CEPH_VOLUME_ALLOW_LOOP_DEVICES": "true",
177
+ }
178
+
179
+ def __init__(self, name: str = ""):
180
+ super().__init__(name=name)
181
+ self.index = 0
182
+ if "_" in self.name:
183
+ self.index = int(self.name.split("_")[-1])
184
+ self.loop_device_count = config["containers"]["testnode"].get(
185
+ "loop_device_count", 1
186
+ )
187
+ self.devices = [self.device_name(i) for i in range(self.loop_device_count)]
188
+
189
+ @property
190
+ def loop_img_dir(self):
191
+ return (Path(config["data_dir"]) / "disk_images").expanduser()
192
+
193
+ @property
194
+ def create_cmd(self):
195
+ return [
196
+ "podman",
197
+ "container",
198
+ "create",
199
+ "--rm",
200
+ "-i",
201
+ "--network",
202
+ "ceph-devstack",
203
+ "--systemd=always",
204
+ "--cgroupns=host",
205
+ "--secret",
206
+ "id_rsa.pub",
207
+ "-p",
208
+ "22",
209
+ "--cap-add",
210
+ ",".join(self.capabilities),
211
+ "--security-opt",
212
+ "unmask=/sys/dev/block",
213
+ "-v",
214
+ "/sys/dev/block:/sys/dev/block",
215
+ "-v",
216
+ "/sys/fs/cgroup:/sys/fs/cgroup",
217
+ "-v",
218
+ "/dev/fuse:/dev/fuse",
219
+ "-v",
220
+ "/dev/disk:/dev/disk",
221
+ # cephadm tries to access these DMI-related files, and by default they
222
+ # have 600 permissions on the host. It appears to be ok if they are
223
+ # empty, though.
224
+ # The below was bizarrely causing this error message:
225
+ # No such file or directory: OCI runtime attempted to invoke a command that was
226
+ # not found
227
+ # That was causing the container to fail to start up.
228
+ "-v",
229
+ "/dev/null:/sys/class/dmi/id/board_serial",
230
+ "-v",
231
+ "/dev/null:/sys/class/dmi/id/chassis_serial",
232
+ "-v",
233
+ "/dev/null:/sys/class/dmi/id/product_serial",
234
+ *self.additional_volumes,
235
+ "--device",
236
+ "/dev/net/tun",
237
+ *[f"--device={device}" for device in self.devices],
238
+ "--name",
239
+ "{name}",
240
+ "{image}",
241
+ ]
242
+
243
+ @property
244
+ def additional_volumes(self):
245
+ volumes = []
246
+ if (
247
+ sshd_config := DEFAULT_CONFIG_PATH.parent.joinpath(
248
+ "sshd_config"
249
+ ).expanduser()
250
+ ) and sshd_config.exists():
251
+ volumes.extend(
252
+ [
253
+ "-v",
254
+ f"{sshd_config}:/etc/ssh/sshd_config.d/teuthology.conf:z",
255
+ ]
256
+ )
257
+ return volumes
258
+
259
+ async def create(self):
260
+ if not await self.exists():
261
+ await self.create_loop_devices()
262
+ await super().create()
263
+
264
+ async def remove(self):
265
+ await super().remove()
266
+ await self.remove_loop_devices()
267
+
268
+ async def create_loop_devices(self):
269
+ for device in self.devices:
270
+ await self.create_loop_device(device)
271
+
272
+ async def remove_loop_devices(self):
273
+ for device in self.devices:
274
+ await self.remove_loop_device(device)
275
+
276
+ async def create_loop_device(self, device: str):
277
+ size = config["containers"]["testnode"]["loop_device_size"]
278
+ os.makedirs(self.loop_img_dir, exist_ok=True)
279
+ proc = await self.cmd(["lsmod", "|", "grep", "loop"])
280
+ if proc and await proc.wait() != 0:
281
+ await self.cmd(["sudo", "modprobe", "loop"])
282
+ loop_img_name = os.path.join(self.loop_img_dir, self.device_image(device))
283
+ await self.remove_loop_device(device)
284
+ device_pos = device.removeprefix("/dev/loop")
285
+ await self.cmd(
286
+ [
287
+ "sudo",
288
+ "mknod",
289
+ "-m700",
290
+ device,
291
+ "b",
292
+ "7",
293
+ device_pos,
294
+ ],
295
+ check=True,
296
+ )
297
+ await self.cmd(
298
+ ["sudo", "chown", f"{os.getuid()}:{os.getgid()}", device],
299
+ check=True,
300
+ )
301
+ await self.cmd(
302
+ [
303
+ "sudo",
304
+ "dd",
305
+ "if=/dev/null",
306
+ f"of={loop_img_name}",
307
+ "bs=1",
308
+ "count=0",
309
+ f"seek={size}",
310
+ ],
311
+ check=True,
312
+ )
313
+ await self.cmd(["sudo", "losetup", device, loop_img_name], check=True)
314
+ await self.cmd(["chcon", "-t", "fixed_disk_device_t", device])
315
+
316
+ async def remove_loop_device(self, device: str):
317
+ loop_img_name = os.path.join(self.loop_img_dir, self.device_image(device))
318
+ if os.path.ismount(device):
319
+ await self.cmd(["umount", device], check=True)
320
+ if host.path_exists(device):
321
+ await self.cmd(["sudo", "losetup", "-d", device])
322
+ await self.cmd(["sudo", "rm", "-f", device], check=True)
323
+ if host.path_exists(loop_img_name):
324
+ os.remove(loop_img_name)
325
+
326
+ def device_name(self, index: int):
327
+ return f"/dev/loop{self.loop_device_count * self.index + index}"
328
+
329
+ def device_image(self, device: str):
330
+ return f"{self.name}-{device.removeprefix('/dev/loop')}"
331
+
332
+
333
+ class Teuthology(Container):
334
+ cmd_vars: List[str] = ["name", "image", "image_tag", "archive_dir"]
335
+
336
+ build_cmd: List[str] = [
337
+ "podman",
338
+ "build",
339
+ "-t",
340
+ "{name}:{image_tag}",
341
+ "-f",
342
+ "./containers/teuthology-dev/Dockerfile",
343
+ ".",
344
+ ]
345
+
346
+ @property
347
+ def create_cmd(self):
348
+ cmd = [
349
+ "podman",
350
+ "container",
351
+ "create",
352
+ "-i",
353
+ "--label",
354
+ f"testnode_count={config['containers']['testnode']['count']}",
355
+ "--network",
356
+ "ceph-devstack",
357
+ "--secret",
358
+ "id_rsa",
359
+ "-v",
360
+ "{archive_dir}:/archive_dir" + ARCHIVE_MOUNT_SUFFIX,
361
+ ]
362
+ ansible_inv = os.environ.get("ANSIBLE_INVENTORY_PATH")
363
+ if ansible_inv:
364
+ cmd += [
365
+ "-v",
366
+ f"{ansible_inv}/inventory:/etc/ansible/hosts",
367
+ "-v",
368
+ f"{ansible_inv}/secrets:/etc/ansible/secrets",
369
+ ]
370
+ ssh_auth_socket = os.environ.get("SSH_AUTH_SOCK")
371
+ if ssh_auth_socket and Path(ssh_auth_socket).exists():
372
+ cmd += [
373
+ "-v",
374
+ f"{ssh_auth_socket}:{ssh_auth_socket}",
375
+ "-e",
376
+ f"SSH_AUTH_SOCK={ssh_auth_socket}",
377
+ ]
378
+ custom_conf = os.environ.get("TEUTHOLOGY_CONF")
379
+ if custom_conf:
380
+ cmd += [
381
+ "-v",
382
+ f"{custom_conf}:/tmp/conf.yaml",
383
+ "-e",
384
+ "TEUTHOLOGY_CONF=/tmp/conf.yaml",
385
+ ]
386
+ teuthology_yaml = os.environ.get("TEUTHOLOGY_YAML")
387
+ if teuthology_yaml:
388
+ cmd += [
389
+ "-v",
390
+ f"{teuthology_yaml}:/root/.teuthology.yaml",
391
+ ]
392
+ cmd += [
393
+ "--name",
394
+ "{name}",
395
+ "{image}",
396
+ ]
397
+ return cmd
398
+
399
+ env_vars = {
400
+ "SSH_PRIVKEY": "",
401
+ "SSH_PRIVKEY_FILE": "",
402
+ "TEUTHOLOGY_MACHINE_TYPE": "",
403
+ "TEUTHOLOGY_TESTNODES": "",
404
+ "TEUTHOLOGY_BRANCH": "",
405
+ "TEUTHOLOGY_CEPH_BRANCH": "",
406
+ "TEUTHOLOGY_CEPH_REPO": "",
407
+ "TEUTHOLOGY_SUITE": "",
408
+ "TEUTHOLOGY_SUITE_BRANCH": "",
409
+ "TEUTHOLOGY_SUITE_REPO": "",
410
+ "TEUTHOLOGY_SUITE_EXTRA_ARGS": "",
411
+ }
412
+
413
+ @property
414
+ def archive_dir(self):
415
+ return Path(config["data_dir"]) / "archive"
416
+
417
+ async def create(self):
418
+ self.archive_dir.expanduser().resolve().mkdir(parents=True, exist_ok=True)
419
+ await super().create()
@@ -0,0 +1,3 @@
1
+ class TooManyJobsFound(Exception):
2
+ def __init__(self, jobs: list[str]):
3
+ self.jobs = jobs
@@ -0,0 +1,90 @@
1
+ from ceph_devstack import logger, PROJECT_ROOT
2
+ from ceph_devstack.requirements import Requirement, FixableRequirement
3
+
4
+
5
+ class HasSudo(Requirement):
6
+ check_cmd = ["sudo", "true"]
7
+ suggest_msg = "sudo access is required"
8
+
9
+
10
+ class LoopControlDeviceExists(FixableRequirement):
11
+ device = "/dev/loop-control"
12
+ check_cmd = ["test", "-e", device]
13
+ suggest_msg = f"{device} does not exist"
14
+ fix_cmd = ["sudo", "modprobe", "loop"]
15
+
16
+
17
+ class LoopControlDeviceWriteable(FixableRequirement):
18
+ device = "/dev/loop-control"
19
+ check_cmd = ["test", "-w", device]
20
+ suggest_msg = f"Cannot write to {device}"
21
+
22
+ async def check(self):
23
+ if not (result := await super().check()):
24
+ group = (
25
+ self.host.run(["stat", "--printf", "%G", self.device])
26
+ .communicate()[0]
27
+ .decode()
28
+ )
29
+ user = self.host.run(["whoami"]).communicate()[0].strip().decode()
30
+ if self.host.type == "local":
31
+ self.fix_cmd = ["sudo", "usermod", "-a", "-G", group, user]
32
+ else:
33
+ self.fix_cmd = ["sudo", "chgrp", user, self.device]
34
+ self.suggest_msg = f"Cannot write to {self.device}"
35
+ return result
36
+
37
+ async def suggest(self):
38
+ await super().suggest()
39
+ if self.host.type == "local":
40
+ logger.warning(
41
+ "Note that group modifications require a logout to take effect."
42
+ )
43
+
44
+
45
+ class SELinuxModule(FixableRequirement):
46
+ def __init__(self):
47
+ fix_cmd = self.fix_cmd_prebuilt
48
+ if self.host.type == "remote":
49
+ fix_cmd = ["podman", "machine", "ssh", "--"] + fix_cmd
50
+ self.fix_cmd = fix_cmd
51
+
52
+ fix_cmd_build = [
53
+ "(sudo",
54
+ "dnf",
55
+ "install",
56
+ "-y",
57
+ "policycoreutils-devel",
58
+ "selinux-policy-devel",
59
+ "&&",
60
+ "cd",
61
+ str(PROJECT_ROOT),
62
+ "&&",
63
+ "make",
64
+ "-f",
65
+ "/usr/share/selinux/devel/Makefile",
66
+ "ceph_devstack.pp",
67
+ "&&",
68
+ "sudo",
69
+ "semodule",
70
+ "-i",
71
+ "ceph_devstack.pp)",
72
+ ]
73
+ fix_cmd_prebuilt = [
74
+ "sudo",
75
+ "semodule",
76
+ "-i",
77
+ str(PROJECT_ROOT / "ceph_devstack.pp"),
78
+ ]
79
+ suggest_msg = (
80
+ "SELinux is in Enforcing mode. To run nested rootless podman "
81
+ "containers, it is necessary to install ceph-devstack's SELinux "
82
+ "module"
83
+ )
84
+
85
+ async def check(self):
86
+ proc = await self.host.arun(["sudo", "semodule", "-l"])
87
+ assert proc.stdout is not None
88
+ await proc.wait()
89
+ out = (await proc.stdout.read()).decode()
90
+ return "ceph_devstack" in out.split("\n")
@@ -0,0 +1,45 @@
1
+ import re
2
+ from datetime import datetime
3
+
4
+ from ceph_devstack.resources.ceph.exceptions import TooManyJobsFound
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_most_recent_run(runs: list[str]) -> str:
18
+ try:
19
+ run_name = next(
20
+ iter(
21
+ sorted(
22
+ (
23
+ dirname
24
+ for dirname in runs
25
+ if RUN_DIRNAME_PATTERN.search(dirname)
26
+ ),
27
+ key=lambda dirname: get_logtimestamp(dirname),
28
+ reverse=True,
29
+ )
30
+ )
31
+ )
32
+ return run_name
33
+ except StopIteration as e:
34
+ raise FileNotFoundError from e
35
+
36
+
37
+ def get_job_id(jobs: list[str]):
38
+ job_dir_pattern = re.compile(r"^\d+$")
39
+ dirs = [d for d in jobs if job_dir_pattern.match(d)]
40
+
41
+ if len(dirs) == 0:
42
+ raise FileNotFoundError
43
+ elif len(dirs) > 1:
44
+ raise TooManyJobsFound(dirs)
45
+ return dirs[0]