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,277 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from packaging.version import parse as parse_version, Version
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from ceph_devstack import config, logger
|
|
9
|
+
from ceph_devstack.host import Host, host, local_host
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Requirement:
|
|
13
|
+
host: Host = host
|
|
14
|
+
check_cmd: List[str]
|
|
15
|
+
|
|
16
|
+
async def evaluate(self) -> bool:
|
|
17
|
+
return await self.check()
|
|
18
|
+
|
|
19
|
+
async def check(self) -> bool:
|
|
20
|
+
proc = await self.host.arun(self.check_cmd)
|
|
21
|
+
return await proc.wait() == 0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FixableRequirement(Requirement):
|
|
25
|
+
fix_cmd: List[str]
|
|
26
|
+
suggest_msg: str
|
|
27
|
+
|
|
28
|
+
async def evaluate(self) -> bool:
|
|
29
|
+
if await self.check() is True:
|
|
30
|
+
return True
|
|
31
|
+
if config["args"].get("fix", False):
|
|
32
|
+
return await self.fix()
|
|
33
|
+
else:
|
|
34
|
+
await self.suggest()
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
async def suggest(self):
|
|
38
|
+
if hasattr(self, "suggest_msg"):
|
|
39
|
+
logger.error(f"{self.suggest_msg}. Try: {shlex.join(self.fix_cmd)}")
|
|
40
|
+
|
|
41
|
+
async def fix(self) -> bool:
|
|
42
|
+
assert self.fix_cmd, "Attempted to fix without a fix command"
|
|
43
|
+
proc = await self.host.arun(self.fix_cmd)
|
|
44
|
+
return await proc.wait() == 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class LocalRequirement(Requirement):
|
|
48
|
+
host = local_host
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PodmanPlatform(Requirement):
|
|
52
|
+
async def check(self):
|
|
53
|
+
result = False
|
|
54
|
+
try:
|
|
55
|
+
podman_info = await self.host.podman_info()
|
|
56
|
+
except FileNotFoundError:
|
|
57
|
+
logger.error("podman not found. Try: dnf install podman")
|
|
58
|
+
return False
|
|
59
|
+
try:
|
|
60
|
+
host_os = (
|
|
61
|
+
podman_info["host"].get("Os") or podman_info["host"]["os"]
|
|
62
|
+
).lower()
|
|
63
|
+
if host_os == "linux":
|
|
64
|
+
result = True
|
|
65
|
+
except KeyError:
|
|
66
|
+
host_os = sys.platform.lower()
|
|
67
|
+
result = False
|
|
68
|
+
if sys.platform == "darwin":
|
|
69
|
+
logger.error(
|
|
70
|
+
"The podman machine (VM) is not running. "
|
|
71
|
+
"Try: podman machine init --now"
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
logger.error(
|
|
75
|
+
"Unknown error trying to query podman. Is podman installed?"
|
|
76
|
+
)
|
|
77
|
+
return result
|
|
78
|
+
if host_os != "linux":
|
|
79
|
+
logger.error("The platform '{host_os}' is not currently supported.")
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class PodmanGraphDriver(Requirement):
|
|
84
|
+
async def check(self):
|
|
85
|
+
podman_info = await self.host.podman_info()
|
|
86
|
+
storage_conf_path = podman_info["store"]["configFile"]
|
|
87
|
+
graph_driver = podman_info["store"]["graphDriverName"]
|
|
88
|
+
if graph_driver == "overlay":
|
|
89
|
+
return True
|
|
90
|
+
else:
|
|
91
|
+
self.suggest_msg = (
|
|
92
|
+
f"The configured graph driver is '{graph_driver}'. "
|
|
93
|
+
f"It must be set to 'overlay' in {storage_conf_path}."
|
|
94
|
+
)
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class KernelVersionForOverlay(Requirement):
|
|
99
|
+
async def check(self):
|
|
100
|
+
kernel_version = self.host.kernel_version()
|
|
101
|
+
version_for_overlay = Version("5.12")
|
|
102
|
+
if kernel_version < version_for_overlay:
|
|
103
|
+
self.suggest_msg = (
|
|
104
|
+
f"Kernel version ({kernel_version}) is too old to support native rootless "
|
|
105
|
+
f"overlayfs (needs {version_for_overlay})"
|
|
106
|
+
)
|
|
107
|
+
return False
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class KernelVersionForCgroupV2(Requirement):
|
|
112
|
+
async def check(self):
|
|
113
|
+
version_for_cgroup = Version("4.15")
|
|
114
|
+
kernel_version = self.host.kernel_version()
|
|
115
|
+
if not kernel_version >= version_for_cgroup:
|
|
116
|
+
self.suggest_msg = (
|
|
117
|
+
f"Kernel version ({kernel_version}) is too old to support cgroup v2 "
|
|
118
|
+
f"(needs {version_for_cgroup})"
|
|
119
|
+
)
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class CgroupV2(FixableRequirement):
|
|
124
|
+
suggest_msg = "cgroup v2 is not enabled"
|
|
125
|
+
fix_cmd = [
|
|
126
|
+
"sudo",
|
|
127
|
+
"grubby",
|
|
128
|
+
"--update-kernel=ALL",
|
|
129
|
+
"--args='systemd.unified_cgroup_hierarchy=1'",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
async def check(self):
|
|
133
|
+
podman_info = await self.host.podman_info()
|
|
134
|
+
return podman_info["host"]["cgroupVersion"] == "v2"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PodmanVersion(Requirement):
|
|
138
|
+
def __init__(self, version: str, msg: str = ""):
|
|
139
|
+
self.required_version = parse_version(version)
|
|
140
|
+
self.msg = msg
|
|
141
|
+
|
|
142
|
+
async def check(self):
|
|
143
|
+
podman_info = await self.host.podman_info()
|
|
144
|
+
podman_version = parse_version(podman_info["version"]["Version"])
|
|
145
|
+
if podman_version < self.required_version:
|
|
146
|
+
if self.msg:
|
|
147
|
+
logger.warning(self.msg)
|
|
148
|
+
return False
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class PodmanRuntime(Requirement):
|
|
153
|
+
async def check(self):
|
|
154
|
+
podman_info = await self.host.podman_info()
|
|
155
|
+
storage_conf_path = podman_info["store"]["configFile"]
|
|
156
|
+
runtime = podman_info["host"]["ociRuntime"]["name"]
|
|
157
|
+
if runtime == "crun":
|
|
158
|
+
return True
|
|
159
|
+
else:
|
|
160
|
+
containers_conf_path = Path(storage_conf_path).parent / "containers.conf"
|
|
161
|
+
cmd = host.cmd(["podman", "system", "reset"])
|
|
162
|
+
logger.error(
|
|
163
|
+
f"The configured runtime is '{runtime}'. "
|
|
164
|
+
f"It must be set to 'crun' in {containers_conf_path}. "
|
|
165
|
+
f"Afterward, run '{cmd}'."
|
|
166
|
+
)
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class SELinuxBoolean(FixableRequirement):
|
|
171
|
+
def __init__(self, boolean_name: str):
|
|
172
|
+
super().__init__()
|
|
173
|
+
self.boolean_name = boolean_name
|
|
174
|
+
self.fix_cmd = ["sudo", "setsebool", "-P", f"{self.boolean_name}=true"]
|
|
175
|
+
self.suggest_msg = f"SELinux boolean '{self.boolean_name}' must be enabled"
|
|
176
|
+
|
|
177
|
+
async def check(self):
|
|
178
|
+
return await self.host.check_selinux_bool(self.boolean_name)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class SysctlValue(FixableRequirement):
|
|
182
|
+
def __init__(self, name: str, min_value: int):
|
|
183
|
+
super().__init__()
|
|
184
|
+
self.key = name
|
|
185
|
+
self.min_value = min_value
|
|
186
|
+
self.fix_cmd = ["sudo", "sysctl", f"{name}={min_value}"]
|
|
187
|
+
|
|
188
|
+
async def check(self):
|
|
189
|
+
current_value = await self.host.get_sysctl_value(self.key)
|
|
190
|
+
self.suggest_msg = f"sysctl setting {self.key} ({current_value}) is too low"
|
|
191
|
+
return current_value >= self.min_value
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class PodmanDNSPlugin(FixableRequirement):
|
|
195
|
+
suggest_msg = "Could not find the podman DNS plugin"
|
|
196
|
+
|
|
197
|
+
def __init__(self):
|
|
198
|
+
os_type = self.host.os_type()
|
|
199
|
+
if os_type == "centos":
|
|
200
|
+
dns_plugin_path = "/usr/libexec/cni/dnsname"
|
|
201
|
+
self.check_cmd = ["test", "-x", dns_plugin_path]
|
|
202
|
+
self.fix_cmd = ["sudo", "dnf", "install", "-y", dns_plugin_path]
|
|
203
|
+
elif os_type in ["ubuntu", "debian"]:
|
|
204
|
+
dns_plugin_path = "/usr/lib/cni/dnsname"
|
|
205
|
+
self.check_cmd = ["test", "-x", dns_plugin_path]
|
|
206
|
+
self.fix_cmd = [
|
|
207
|
+
"sudo",
|
|
208
|
+
"apt",
|
|
209
|
+
"install",
|
|
210
|
+
"-y",
|
|
211
|
+
"golang-github-containernetworking-plugin-dnsname",
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class FuseOverlayfsPresence(FixableRequirement):
|
|
216
|
+
check_cmd = ["command", "-v", "fuse-overlayfs"]
|
|
217
|
+
suggest_msg = "Could not find fuse-overlayfs"
|
|
218
|
+
fix_cmd = ["sudo", "dnf", "install", "-y", "fuse-overlayfs"]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class AppArmorProfile(FixableRequirement):
|
|
222
|
+
_profile_path = "/etc/apparmor.d/local/unix-chkpwd"
|
|
223
|
+
_profile_content = '"capability dac_override,"'
|
|
224
|
+
check_cmd = ["test", "-f", _profile_path]
|
|
225
|
+
suggest_msg = "Did not find required apparmor profile"
|
|
226
|
+
fix_cmd = [
|
|
227
|
+
"sudo",
|
|
228
|
+
"bash",
|
|
229
|
+
"-c",
|
|
230
|
+
f"echo -e {_profile_content} > {_profile_path} && systemctl reload apparmor",
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
async def check_requirements():
|
|
235
|
+
if not await PodmanPlatform().evaluate():
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
result = True
|
|
239
|
+
# kernel and podman versions for native overlay filesystem
|
|
240
|
+
result = result and await PodmanGraphDriver().evaluate()
|
|
241
|
+
podman_overlay_version = "3.10"
|
|
242
|
+
podman_version_overlay = await PodmanVersion(
|
|
243
|
+
podman_overlay_version,
|
|
244
|
+
"Podman version is too old for rootless native overlayfs (needs {podman_overlay_version})",
|
|
245
|
+
).evaluate()
|
|
246
|
+
needs_fuse = not (
|
|
247
|
+
await KernelVersionForOverlay().evaluate() and podman_version_overlay
|
|
248
|
+
)
|
|
249
|
+
# if not using native overlay, we need fuse-overlayfs
|
|
250
|
+
if needs_fuse:
|
|
251
|
+
result = result and await FuseOverlayfsPresence().evaluate()
|
|
252
|
+
|
|
253
|
+
# cgroup v2
|
|
254
|
+
if not await CgroupV2().evaluate():
|
|
255
|
+
result = result and await KernelVersionForCgroupV2().evaluate()
|
|
256
|
+
|
|
257
|
+
# runtime
|
|
258
|
+
result = result and await PodmanRuntime().evaluate()
|
|
259
|
+
|
|
260
|
+
# SELinux
|
|
261
|
+
if await host.selinux_enforcing():
|
|
262
|
+
result = result and await SELinuxBoolean("container_manage_cgroup").evaluate()
|
|
263
|
+
result = result and await SELinuxBoolean("container_use_devices").evaluate()
|
|
264
|
+
|
|
265
|
+
# AppArmor
|
|
266
|
+
if await host.apparmor_enabled():
|
|
267
|
+
result = result and await AppArmorProfile().evaluate()
|
|
268
|
+
|
|
269
|
+
# podman DNS plugin
|
|
270
|
+
if not await PodmanVersion("5.0").evaluate():
|
|
271
|
+
result = result and await PodmanDNSPlugin().evaluate()
|
|
272
|
+
|
|
273
|
+
# sysctl settings for OSD
|
|
274
|
+
result = result and await SysctlValue("fs.aio-max-nr", 2097152).evaluate()
|
|
275
|
+
result = result and await SysctlValue("kernel.pid_max", 4194304).evaluate()
|
|
276
|
+
|
|
277
|
+
return result
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from subprocess import CalledProcessError
|
|
10
|
+
from typing import List, Dict, Set
|
|
11
|
+
|
|
12
|
+
from ceph_devstack.host import host, local_host
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DevStack:
|
|
16
|
+
def __init__(self, args: argparse.Namespace):
|
|
17
|
+
self.args = args
|
|
18
|
+
self.env: Dict[str, str] = {}
|
|
19
|
+
|
|
20
|
+
def choose_teuthology_branch(self):
|
|
21
|
+
branch = os.environ.get("TEUTHOLOGY_BRANCH", self.get_current_branch())
|
|
22
|
+
self.env["TEUTH_BRANCH"] = branch
|
|
23
|
+
return branch
|
|
24
|
+
|
|
25
|
+
def get_current_branch(self, repo_path: str = "."):
|
|
26
|
+
return subprocess.check_output(
|
|
27
|
+
["git", "branch", "--show-current"],
|
|
28
|
+
cwd=repo_path,
|
|
29
|
+
).decode()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PodmanResource:
|
|
33
|
+
cwd = "."
|
|
34
|
+
exists_cmd: List[str] = []
|
|
35
|
+
create_cmd: List[str] = []
|
|
36
|
+
remove_cmd: List[str] = []
|
|
37
|
+
start_cmd: List[str] = []
|
|
38
|
+
stop_cmd: List[str] = []
|
|
39
|
+
cmd_vars: List[str] = ["name"]
|
|
40
|
+
log: Dict[str, Set[str]] = {}
|
|
41
|
+
_name: str | None = None
|
|
42
|
+
|
|
43
|
+
def __init__(self, name: str = ""):
|
|
44
|
+
if name:
|
|
45
|
+
self._name = name
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def name(self) -> str:
|
|
49
|
+
if self._name is not None:
|
|
50
|
+
return self._name
|
|
51
|
+
return self.__class__.__name__.lower()
|
|
52
|
+
|
|
53
|
+
async def cmd(
|
|
54
|
+
self,
|
|
55
|
+
args: List[str],
|
|
56
|
+
check: bool = False,
|
|
57
|
+
force_local: bool = False,
|
|
58
|
+
stream_output: bool = False,
|
|
59
|
+
) -> asyncio.subprocess.Process:
|
|
60
|
+
exec_host = local_host if force_local else host
|
|
61
|
+
proc = await exec_host.arun(
|
|
62
|
+
args,
|
|
63
|
+
cwd=Path(self.cwd),
|
|
64
|
+
stream_output=stream_output,
|
|
65
|
+
)
|
|
66
|
+
assert proc.stderr is not None
|
|
67
|
+
assert proc.stdout is not None
|
|
68
|
+
returncode = await proc.wait()
|
|
69
|
+
if check and returncode != 0:
|
|
70
|
+
# out = await proc.stderr.read()
|
|
71
|
+
# logger.error(out.decode())
|
|
72
|
+
raise CalledProcessError(cmd=args, returncode=returncode)
|
|
73
|
+
return proc
|
|
74
|
+
|
|
75
|
+
def format_cmd(self, args: List):
|
|
76
|
+
vars = {}
|
|
77
|
+
for k in self.cmd_vars:
|
|
78
|
+
v = getattr(self, k, None)
|
|
79
|
+
if v is not None:
|
|
80
|
+
if isinstance(v, Path):
|
|
81
|
+
v = v.expanduser()
|
|
82
|
+
vars[k] = v
|
|
83
|
+
return [s.format(**vars) for s in args]
|
|
84
|
+
|
|
85
|
+
async def apply(self, action: str):
|
|
86
|
+
method = getattr(self, action, None)
|
|
87
|
+
if method is None:
|
|
88
|
+
return
|
|
89
|
+
await method()
|
|
90
|
+
|
|
91
|
+
async def inspect(self):
|
|
92
|
+
proc = await self.cmd(self.format_cmd(self.exists_cmd))
|
|
93
|
+
out, err = await proc.communicate()
|
|
94
|
+
return json.loads(out)
|
|
95
|
+
return json.loads(proc.stdout.read())
|
|
96
|
+
if proc.stdout is None:
|
|
97
|
+
return {}
|
|
98
|
+
return json.loads((await proc.stdout.read()).decode())
|
|
99
|
+
|
|
100
|
+
async def exists(self):
|
|
101
|
+
if not self.exists_cmd:
|
|
102
|
+
return False
|
|
103
|
+
proc = await self.cmd(self.format_cmd(self.exists_cmd), check=False)
|
|
104
|
+
return await proc.wait() == 0
|
|
105
|
+
|
|
106
|
+
async def create(self):
|
|
107
|
+
if not await self.exists():
|
|
108
|
+
await self.cmd(self.format_cmd(self.create_cmd), check=True)
|
|
109
|
+
|
|
110
|
+
async def remove(self):
|
|
111
|
+
await self.cmd(self.format_cmd(self.remove_cmd))
|
|
112
|
+
|
|
113
|
+
def __repr__(self):
|
|
114
|
+
param_str = "" if self._name is None else f'name="{self._name}"'
|
|
115
|
+
return f"{self.__class__.__name__}({param_str})"
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import os
|
|
4
|
+
import pathlib
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
from subprocess import CalledProcessError
|
|
8
|
+
|
|
9
|
+
from ceph_devstack import config, logger
|
|
10
|
+
from ceph_devstack.host import host
|
|
11
|
+
from ceph_devstack.resources.misc import Secret, Network
|
|
12
|
+
from ceph_devstack.resources.ceph.containers import (
|
|
13
|
+
Postgres,
|
|
14
|
+
Beanstalk,
|
|
15
|
+
Paddles,
|
|
16
|
+
Pulpito,
|
|
17
|
+
TestNode,
|
|
18
|
+
Teuthology,
|
|
19
|
+
Archive,
|
|
20
|
+
)
|
|
21
|
+
from ceph_devstack.resources.ceph.requirements import (
|
|
22
|
+
HasSudo,
|
|
23
|
+
LoopControlDeviceExists,
|
|
24
|
+
LoopControlDeviceWriteable,
|
|
25
|
+
SELinuxModule,
|
|
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
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SSHKeyPair(Secret):
|
|
32
|
+
_name = "id_rsa"
|
|
33
|
+
cmd_vars = ["name", "privkey_path", "pubkey_path"]
|
|
34
|
+
privkey_path = "id_rsa"
|
|
35
|
+
pubkey_path = "id_rsa.pub"
|
|
36
|
+
exists_cmds = [
|
|
37
|
+
["podman", "secret", "inspect", "{name}"],
|
|
38
|
+
["podman", "secret", "inspect", "{name}.pub"],
|
|
39
|
+
]
|
|
40
|
+
create_cmds = [
|
|
41
|
+
["podman", "secret", "create", "{name}", "{privkey_path}"],
|
|
42
|
+
["podman", "secret", "create", "{name}.pub", "{pubkey_path}"],
|
|
43
|
+
]
|
|
44
|
+
remove_cmds = [
|
|
45
|
+
["podman", "secret", "rm", "{name}"],
|
|
46
|
+
["podman", "secret", "rm", "{name}.pub"],
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
async def exists(self):
|
|
50
|
+
for exists_cmd in self.exists_cmds:
|
|
51
|
+
proc = await self.cmd(self.format_cmd(exists_cmd), check=False)
|
|
52
|
+
if await proc.wait():
|
|
53
|
+
return False
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
async def create(self):
|
|
57
|
+
if await self.exists():
|
|
58
|
+
return
|
|
59
|
+
await self._get_ssh_keys()
|
|
60
|
+
for create_cmd in self.create_cmds:
|
|
61
|
+
await self.cmd(self.format_cmd(create_cmd), check=True)
|
|
62
|
+
|
|
63
|
+
async def remove(self):
|
|
64
|
+
for remove_cmd in self.remove_cmds:
|
|
65
|
+
await self.cmd(self.format_cmd(remove_cmd))
|
|
66
|
+
|
|
67
|
+
async def _get_ssh_keys(self):
|
|
68
|
+
privkey_path = os.environ.get("SSH_PRIVKEY_PATH")
|
|
69
|
+
self.pubkey_path = "/dev/null"
|
|
70
|
+
if not privkey_path:
|
|
71
|
+
temp_dir = tempfile.mkdtemp(
|
|
72
|
+
prefix="teuthology-ssh-key-",
|
|
73
|
+
dir="/tmp",
|
|
74
|
+
)
|
|
75
|
+
privkey_path = pathlib.Path(temp_dir) / self.__class__.privkey_path
|
|
76
|
+
await self.cmd(
|
|
77
|
+
["ssh-keygen", "-t", "rsa", "-N", "", "-f", str(privkey_path)],
|
|
78
|
+
check=True,
|
|
79
|
+
force_local=True,
|
|
80
|
+
)
|
|
81
|
+
self.pubkey_path = f"{privkey_path}.pub"
|
|
82
|
+
self.privkey_path = privkey_path
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class CephDevStackNetwork(Network):
|
|
86
|
+
_name = "ceph-devstack"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CephDevStack:
|
|
90
|
+
networks = [CephDevStackNetwork]
|
|
91
|
+
secrets = [SSHKeyPair]
|
|
92
|
+
|
|
93
|
+
def __init__(self):
|
|
94
|
+
services = [
|
|
95
|
+
Postgres,
|
|
96
|
+
Paddles,
|
|
97
|
+
Beanstalk,
|
|
98
|
+
Pulpito,
|
|
99
|
+
Teuthology,
|
|
100
|
+
TestNode,
|
|
101
|
+
Archive,
|
|
102
|
+
]
|
|
103
|
+
self.service_specs = {}
|
|
104
|
+
for service in services:
|
|
105
|
+
name = service.__name__.lower()
|
|
106
|
+
count = config["containers"][name].get("count", 1)
|
|
107
|
+
if count == 0:
|
|
108
|
+
continue
|
|
109
|
+
self.service_specs[name] = {
|
|
110
|
+
"obj": service,
|
|
111
|
+
"count": count,
|
|
112
|
+
}
|
|
113
|
+
if count == 1:
|
|
114
|
+
self.service_specs[name]["objects"] = [service()]
|
|
115
|
+
elif count > 1:
|
|
116
|
+
self.service_specs[name]["objects"] = [
|
|
117
|
+
service(name=f"{name}_{i}") for i in range(count)
|
|
118
|
+
]
|
|
119
|
+
if postgres_spec := self.service_specs.get("postgres"):
|
|
120
|
+
postgres_obj = postgres_spec["objects"][0]
|
|
121
|
+
paddles_obj = self.service_specs["paddles"]["objects"][0]
|
|
122
|
+
paddles_obj.env_vars["PADDLES_SQLALCHEMY_URL"] = (
|
|
123
|
+
postgres_obj.paddles_sqla_url
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
async def check_requirements(self):
|
|
127
|
+
result = True
|
|
128
|
+
|
|
129
|
+
result = has_sudo = await HasSudo().evaluate()
|
|
130
|
+
result = result and await LoopControlDeviceExists().evaluate()
|
|
131
|
+
result = result and await LoopControlDeviceWriteable().evaluate()
|
|
132
|
+
|
|
133
|
+
# Check for SELinux being enabled and Enforcing; then check for the
|
|
134
|
+
# presence of our module. If necessary, inform the user and instruct
|
|
135
|
+
# them how to build and install.
|
|
136
|
+
if has_sudo and await host.selinux_enforcing():
|
|
137
|
+
result = result and await SELinuxModule().evaluate()
|
|
138
|
+
|
|
139
|
+
for name, obj in config["containers"].items():
|
|
140
|
+
if (repo := obj.get("repo")) and not host.path_exists(repo):
|
|
141
|
+
result = False
|
|
142
|
+
logger.error(f"Repo for {name} not found at {repo}")
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
async def apply(self, action):
|
|
146
|
+
return await getattr(self, action)()
|
|
147
|
+
|
|
148
|
+
async def pull(self):
|
|
149
|
+
logger.info("Pulling images...")
|
|
150
|
+
for spec in self.service_specs.values():
|
|
151
|
+
await spec["objects"][0].pull()
|
|
152
|
+
|
|
153
|
+
async def build(self):
|
|
154
|
+
logger.info("Building images...")
|
|
155
|
+
for spec in self.service_specs.values():
|
|
156
|
+
await spec["objects"][0].build()
|
|
157
|
+
|
|
158
|
+
async def create(self):
|
|
159
|
+
logger.info("Creating containers...")
|
|
160
|
+
await CephDevStackNetwork().create()
|
|
161
|
+
await SSHKeyPair().create()
|
|
162
|
+
containers = []
|
|
163
|
+
for spec in self.service_specs.values():
|
|
164
|
+
for object in spec["objects"]:
|
|
165
|
+
containers.append(object.create())
|
|
166
|
+
await asyncio.gather(*containers)
|
|
167
|
+
|
|
168
|
+
async def start(self):
|
|
169
|
+
await self.create()
|
|
170
|
+
logger.info("Starting containers...")
|
|
171
|
+
for spec in self.service_specs.values():
|
|
172
|
+
for object in spec["objects"]:
|
|
173
|
+
await object.start()
|
|
174
|
+
logger.info(
|
|
175
|
+
"All containers are running. To monitor teuthology, try running: podman "
|
|
176
|
+
"logs -f teuthology"
|
|
177
|
+
)
|
|
178
|
+
hostname = host.hostname()
|
|
179
|
+
logger.info(f"View test results at http://{hostname}:8081/")
|
|
180
|
+
|
|
181
|
+
async def stop(self):
|
|
182
|
+
logger.info("Stopping containers...")
|
|
183
|
+
containers = []
|
|
184
|
+
for spec in self.service_specs.values():
|
|
185
|
+
for object in spec["objects"]:
|
|
186
|
+
containers.append(object.stop())
|
|
187
|
+
await asyncio.gather(*containers)
|
|
188
|
+
|
|
189
|
+
async def remove(self):
|
|
190
|
+
logger.info("Removing containers...")
|
|
191
|
+
containers = []
|
|
192
|
+
for spec in self.service_specs.values():
|
|
193
|
+
for object in spec["objects"]:
|
|
194
|
+
containers.append(object.remove())
|
|
195
|
+
await asyncio.gather(*containers)
|
|
196
|
+
await CephDevStackNetwork().remove()
|
|
197
|
+
await SSHKeyPair().remove()
|
|
198
|
+
|
|
199
|
+
async def watch(self):
|
|
200
|
+
logger.info("Watching containers; will replace any that are stopped")
|
|
201
|
+
containers = []
|
|
202
|
+
for spec in self.service_specs.values():
|
|
203
|
+
if not spec["count"] > 0:
|
|
204
|
+
continue
|
|
205
|
+
for object in spec["objects"]:
|
|
206
|
+
containers.append(object)
|
|
207
|
+
logger.info(f"Watching {containers}")
|
|
208
|
+
while True:
|
|
209
|
+
try:
|
|
210
|
+
for container in containers:
|
|
211
|
+
with contextlib.suppress(CalledProcessError):
|
|
212
|
+
if not await container.exists():
|
|
213
|
+
logger.info(
|
|
214
|
+
f"Container {container.name} was removed; replacing"
|
|
215
|
+
)
|
|
216
|
+
await container.create()
|
|
217
|
+
await container.start()
|
|
218
|
+
elif not await container.is_running():
|
|
219
|
+
logger.info(
|
|
220
|
+
f"Container {container.name} stopped; restarting"
|
|
221
|
+
)
|
|
222
|
+
await container.start()
|
|
223
|
+
except KeyboardInterrupt:
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
async def wait(self, container_name: str):
|
|
227
|
+
for spec in self.service_specs.values():
|
|
228
|
+
for object in spec["objects"]:
|
|
229
|
+
if object.name == container_name:
|
|
230
|
+
return await object.wait()
|
|
231
|
+
logger.error(f"Could not find container {container_name}")
|
|
232
|
+
return 1
|
|
233
|
+
|
|
234
|
+
async def logs(self, run_name: str = "", job_id: str = "", locate: bool = False):
|
|
235
|
+
try:
|
|
236
|
+
log_file = self.get_log_file(run_name, job_id)
|
|
237
|
+
except FileNotFoundError:
|
|
238
|
+
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
|
+
else:
|
|
245
|
+
if locate:
|
|
246
|
+
print(log_file)
|
|
247
|
+
else:
|
|
248
|
+
buffer_size = 8 * 1024
|
|
249
|
+
with open(log_file) as f:
|
|
250
|
+
while chunk := f.read(buffer_size):
|
|
251
|
+
print(chunk, end="")
|
|
252
|
+
|
|
253
|
+
def get_log_file(self, run_name: str = "", job_id: str = ""):
|
|
254
|
+
archive_dir = Teuthology().archive_dir.expanduser()
|
|
255
|
+
|
|
256
|
+
if not run_name:
|
|
257
|
+
run_name = get_most_recent_run(os.listdir(archive_dir))
|
|
258
|
+
run_dir = archive_dir.joinpath(run_name)
|
|
259
|
+
|
|
260
|
+
if not job_id:
|
|
261
|
+
job_id = get_job_id(os.listdir(run_dir))
|
|
262
|
+
|
|
263
|
+
log_file = run_dir.joinpath(job_id, "teuthology.log")
|
|
264
|
+
if not log_file.exists():
|
|
265
|
+
raise FileNotFoundError
|
|
266
|
+
return log_file
|