Merge ~adam-collard/maas-ci/+git/system-tests:lxd-instance-facade into ~maas-committers/maas-ci/+git/system-tests:master
- Git
- lp:~adam-collard/maas-ci/+git/system-tests
- lxd-instance-facade
- Merge into master
Proposed by
Adam Collard
Status: | Merged |
---|---|
Approved by: | Adam Collard |
Approved revision: | ceb52ed309df1e1b2e91c94f943eac045193e75c |
Merge reported by: | MAAS Lander |
Merged at revision: | not available |
Proposed branch: | ~adam-collard/maas-ci/+git/system-tests:lxd-instance-facade |
Merge into: | ~maas-committers/maas-ci/+git/system-tests:master |
Diff against target: |
1465 lines (+372/-297) 14 files modified
systemtests/api.py (+6/-14) systemtests/collect_sos_report/test_collect.py (+27/-6) systemtests/conftest.py (+5/-6) systemtests/device_config.py (+2/-0) systemtests/env_builder/test_basic.py (+21/-18) systemtests/fixtures.py (+90/-116) systemtests/lxd.py (+138/-31) systemtests/o11y.py (+10/-14) systemtests/region.py (+5/-5) systemtests/state.py (+5/-7) systemtests/tests_per_machine/test_hardware_sync.py (+24/-38) systemtests/tls.py (+8/-12) systemtests/utils.py (+14/-3) systemtests/vault.py (+17/-27) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Alexsander de Souza | Approve | ||
MAAS Lander | Approve | ||
Review via email: mp+435762@code.launchpad.net |
Commit message
Add lxd.Instance facade for interacting with a particular LXD instance
Description of the change
To post a comment you must log in.
Revision history for this message
Alexsander de Souza (alexsander-souza) wrote : | # |
+1
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/systemtests/api.py b/systemtests/api.py |
2 | index 568d6fa..20cc361 100644 |
3 | --- a/systemtests/api.py |
4 | +++ b/systemtests/api.py |
5 | @@ -1,7 +1,6 @@ |
6 | from __future__ import annotations |
7 | |
8 | import json |
9 | -from functools import partial |
10 | from logging import getLogger |
11 | from subprocess import CalledProcessError |
12 | from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, TypedDict, Union |
13 | @@ -12,7 +11,7 @@ from .utils import wait_for_machine |
14 | if TYPE_CHECKING: |
15 | from logging import Logger |
16 | |
17 | - from . import lxd |
18 | + from .lxd import Instance |
19 | |
20 | LOG = getLogger("systemtests.api") |
21 | |
22 | @@ -84,34 +83,31 @@ class UnauthenticatedMAASAPIClient: |
23 | |
24 | MAAS_CMD = ["sudo", "-u", "ubuntu", "maas"] |
25 | |
26 | - def __init__(self, url: str, maas_container: str, lxd: lxd.CLILXD): |
27 | + def __init__(self, url: str, maas_container: Instance): |
28 | self.url = url |
29 | self.maas_container = maas_container |
30 | - self.lxd = lxd |
31 | - self.pull_file = partial(lxd.pull_file, maas_container) |
32 | - self.push_file = partial(lxd.push_file, maas_container) |
33 | |
34 | def __repr__(self) -> str: |
35 | return f"<UnauthenticatedMAASAPIClient for {self.url!r}>" |
36 | |
37 | @property |
38 | def logger(self) -> Logger: |
39 | - return self.lxd.logger |
40 | + return self.maas_container.logger |
41 | |
42 | @logger.setter |
43 | def logger(self, logger: Logger) -> None: |
44 | - self.lxd.logger = logger |
45 | + self.maas_container.logger = logger |
46 | |
47 | def execute(self, cmd: list[str], base_cmd: Optional[list[str]] = None) -> str: |
48 | __tracebackhide__ = True |
49 | if base_cmd is None: |
50 | base_cmd = self.MAAS_CMD |
51 | - result = self.lxd.execute(self.maas_container, base_cmd + cmd) |
52 | + result = self.maas_container.execute(base_cmd + cmd) |
53 | return result.stdout |
54 | |
55 | def quietly_execute(self, cmd: list[str]) -> str: |
56 | __tracebackhide__ = True |
57 | - result = self.lxd.quietly_execute(self.maas_container, self.MAAS_CMD + cmd) |
58 | + result = self.maas_container.quietly_execute(self.MAAS_CMD + cmd) |
59 | return result.stdout |
60 | |
61 | def log_in(self, session: str, token: str) -> tuple[str, AuthenticatedAPIClient]: |
62 | @@ -133,10 +129,6 @@ class AuthenticatedAPIClient: |
63 | return f"<AuthenticatedAPIClient for {self.api_client.url!r}>" |
64 | |
65 | @property |
66 | - def lxd(self) -> lxd.CLILXD: |
67 | - return self.api_client.lxd |
68 | - |
69 | - @property |
70 | def logger(self) -> Logger: |
71 | return self.api_client.logger |
72 | |
73 | diff --git a/systemtests/collect_sos_report/test_collect.py b/systemtests/collect_sos_report/test_collect.py |
74 | index ff625da..d8b1e5e 100644 |
75 | --- a/systemtests/collect_sos_report/test_collect.py |
76 | +++ b/systemtests/collect_sos_report/test_collect.py |
77 | @@ -1,12 +1,33 @@ |
78 | import contextlib |
79 | import subprocess |
80 | -from logging import getLogger |
81 | +import tempfile |
82 | +from pathlib import Path |
83 | |
84 | -from ..lxd import get_lxd |
85 | +from ..lxd import Instance |
86 | |
87 | |
88 | -def test_collect_sos_report(maas_container: str) -> None: |
89 | - lxd = get_lxd(getLogger("collect_sos_report")) |
90 | - assert lxd.container_exists(maas_container) |
91 | +def collect_sos_report(instance: Instance, output: Path) -> None: |
92 | + container_tmp = "/tmp/sosreport" |
93 | + output_dir = output / "sosreport" |
94 | + instance.execute(["apt", "install", "--yes", "sosreport"]) |
95 | + instance.execute(["rm", "-rf", container_tmp]) |
96 | + instance.execute(["mkdir", "-p", container_tmp]) |
97 | + instance.execute( |
98 | + ["sos", "report", "--batch", "-o", "maas", "--tmp-dir", container_tmp] |
99 | + ) |
100 | + output_dir.mkdir(parents=True, exist_ok=True) |
101 | + journalctl = instance.execute( |
102 | + ["journalctl", "--unit=vault", "--no-pager", "--output=cat"] |
103 | + ) |
104 | + if journalctl.stdout: |
105 | + (output_dir / "vault-journal").write_text(journalctl.stdout) |
106 | + with tempfile.TemporaryDirectory(prefix="sosreport") as tempdir: |
107 | + instance.files[container_tmp].pull(tempdir) |
108 | + for f in (Path(tempdir) / "sosreport").iterdir(): |
109 | + f.rename(output_dir / f.name) |
110 | + |
111 | + |
112 | +def test_collect_sos_report(maas_container: Instance) -> None: |
113 | + assert maas_container.exists() |
114 | with contextlib.suppress(subprocess.CalledProcessError): |
115 | - lxd.collect_sos_report(maas_container, ".") |
116 | + collect_sos_report(maas_container, Path(".")) |
117 | diff --git a/systemtests/conftest.py b/systemtests/conftest.py |
118 | index 39ef76e..39c59a0 100644 |
119 | --- a/systemtests/conftest.py |
120 | +++ b/systemtests/conftest.py |
121 | @@ -30,7 +30,7 @@ from .fixtures import ( |
122 | vault, |
123 | zone, |
124 | ) |
125 | -from .lxd import get_lxd |
126 | +from .lxd import Instance, get_lxd |
127 | from .machine_config import MachineConfig |
128 | from .state import ( |
129 | authenticated_admin, |
130 | @@ -175,20 +175,19 @@ def hardware_sync_machine( |
131 | machine = authenticated_admin.list_machines(mac_address=machine_config.mac_address)[ |
132 | 0 |
133 | ] |
134 | + instance = Instance(get_lxd(LOG), machine_config.name) |
135 | assert machine["status"] == STATUS_READY |
136 | yield HardwareSyncMachine( |
137 | name=machine_config.name, |
138 | machine=machine, |
139 | devices_config=machine_config.devices_config, |
140 | + instance=instance, |
141 | ) |
142 | if machine_config.power_type == "lxd": |
143 | - lxd = get_lxd(LOG) |
144 | - current_devices = lxd.list_instance_devices(machine_config.name) |
145 | + current_devices = instance.list_devices() |
146 | for additional_device in machine_config.devices_config: |
147 | if additional_device["device_name"] in current_devices: |
148 | - lxd.remove_instance_device( |
149 | - machine_config.name, additional_device["device_name"] |
150 | - ) |
151 | + instance.remove_device(additional_device["device_name"]) |
152 | authenticated_admin.release_machine(machine) |
153 | wait_for_machine( |
154 | authenticated_admin, |
155 | diff --git a/systemtests/device_config.py b/systemtests/device_config.py |
156 | index 41d883b..342f8aa 100644 |
157 | --- a/systemtests/device_config.py |
158 | +++ b/systemtests/device_config.py |
159 | @@ -4,6 +4,7 @@ from dataclasses import dataclass |
160 | from typing import Dict |
161 | |
162 | from .api import Machine |
163 | +from .lxd import Instance |
164 | |
165 | DeviceConfig = Dict[str, str] |
166 | |
167 | @@ -13,3 +14,4 @@ class HardwareSyncMachine: |
168 | name: str |
169 | machine: Machine |
170 | devices_config: tuple[DeviceConfig, ...] |
171 | + instance: Instance |
172 | diff --git a/systemtests/env_builder/test_basic.py b/systemtests/env_builder/test_basic.py |
173 | index 5f53438..265fa06 100644 |
174 | --- a/systemtests/env_builder/test_basic.py |
175 | +++ b/systemtests/env_builder/test_basic.py |
176 | @@ -9,8 +9,10 @@ from urllib.request import urlopen |
177 | import pytest |
178 | from retry import retry |
179 | |
180 | -from systemtests.lxd import get_lxd |
181 | +from systemtests.lxd import Instance, get_lxd |
182 | from systemtests.utils import ( |
183 | + UnexpectedMachineStatus, |
184 | + debug_lxd_vm, |
185 | randomstring, |
186 | retries, |
187 | wait_for_machine, |
188 | @@ -118,13 +120,10 @@ class TestSetup: |
189 | """Ensure that we have a Ready VM at the end.""" |
190 | lxd = get_lxd(logger=testlog) |
191 | vm_name = instance_config.name |
192 | - try: |
193 | - lxd.instance_status(vm_name) |
194 | - except ValueError: |
195 | - pass |
196 | - else: |
197 | + instance = Instance(lxd, vm_name) |
198 | + if instance.exists(): |
199 | # Force delete the VM so we know we're starting clean |
200 | - lxd.delete(vm_name) |
201 | + instance.delete() |
202 | |
203 | # Need to create a network device with a hwaddr |
204 | config: dict[str, str] = {"security.secureboot": "false"} |
205 | @@ -133,7 +132,7 @@ class TestSetup: |
206 | if instance_config.mac_address: |
207 | config["volatile.eth0.hwaddr"] = instance_config.mac_address |
208 | |
209 | - lxd.create_vm(vm_name, config) |
210 | + instance = lxd.create_vm(vm_name, config) |
211 | |
212 | mac_address = instance_config.mac_address |
213 | |
214 | @@ -145,29 +144,33 @@ class TestSetup: |
215 | else: |
216 | # Machine not registered, let's boot it up |
217 | @retry(tries=5, delay=5, backoff=1.2, logger=testlog) |
218 | - def _boot_vm(vm_name: str) -> None: |
219 | - status = lxd.instance_status(vm_name) |
220 | + def _boot_vm(vm: Instance) -> None: |
221 | + status = instance.status() |
222 | if status == "RUNNING": |
223 | - testlog.debug(f"{vm_name} is already running, restarting") |
224 | - lxd.restart(vm_name) |
225 | + testlog.debug(f"{instance.name} is already running, restarting") |
226 | + instance.restart() |
227 | elif status == "STOPPED": |
228 | - testlog.debug(f"{vm_name} is stopped, starting") |
229 | + testlog.debug(f"{instance.name} is stopped, starting") |
230 | try: |
231 | - lxd.start(vm_name) |
232 | + instance.start() |
233 | except CalledProcessError: |
234 | - lxd._run(["lxc", "info", "--show-log", vm_name]) |
235 | + debug_lxd_vm(vm_name, testlog) |
236 | raise |
237 | else: |
238 | assert False, f"Don't know how to handle lxd_vm status: {status}" |
239 | |
240 | - _boot_vm(vm_name) |
241 | + _boot_vm(instance) |
242 | try: |
243 | - vm_status = lxd.instance_status(vm_name) |
244 | + vm_status = instance.status() |
245 | except ValueError: |
246 | vm_status = "not available" |
247 | testlog.debug(f"{vm_name} is {vm_status}") |
248 | |
249 | - machine = wait_for_new_machine(maas_api_client, mac_address, vm_name) |
250 | + try: |
251 | + machine = wait_for_new_machine(maas_api_client, mac_address, vm_name) |
252 | + except UnexpectedMachineStatus: |
253 | + debug_lxd_vm(vm_name, testlog) |
254 | + raise |
255 | |
256 | # Make sure we have power parameters set |
257 | if not machine["power_type"]: |
258 | diff --git a/systemtests/fixtures.py b/systemtests/fixtures.py |
259 | index eff8651..ba9bbb9 100644 |
260 | --- a/systemtests/fixtures.py |
261 | +++ b/systemtests/fixtures.py |
262 | @@ -2,54 +2,50 @@ from __future__ import annotations |
263 | |
264 | import io |
265 | import os |
266 | -from functools import partial |
267 | -from logging import StreamHandler, getLogger |
268 | +from logging import Logger, StreamHandler, getLogger |
269 | from textwrap import dedent |
270 | -from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, TextIO |
271 | +from typing import Any, Iterator, Optional, TextIO |
272 | |
273 | import paramiko |
274 | import pytest |
275 | import yaml |
276 | from pytest_steps import one_fixture_per_step |
277 | |
278 | -from .api import UnauthenticatedMAASAPIClient |
279 | +from .api import AuthenticatedAPIClient, UnauthenticatedMAASAPIClient |
280 | from .config import ADMIN_EMAIL, ADMIN_PASSWORD, ADMIN_USER |
281 | -from .lxd import CLILXD, get_lxd |
282 | +from .lxd import Instance, get_lxd |
283 | from .o11y import setup_o11y |
284 | from .region import MAASRegion |
285 | from .tls import MAAS_CONTAINER_CERTS_PATH, setup_tls |
286 | from .vault import Vault, VaultNotReadyError, setup_vault |
287 | |
288 | -if TYPE_CHECKING: |
289 | - from logging import Logger |
290 | - |
291 | - from .api import AuthenticatedAPIClient |
292 | LOG_NAME = "systemtests.fixtures" |
293 | |
294 | LXD_PROFILE = os.environ.get("MAAS_SYSTEMTESTS_LXD_PROFILE", "prof-maas-lab") |
295 | |
296 | |
297 | def _add_maas_ppa( |
298 | - exec_on_container: Callable[..., Any], |
299 | + instance: Instance, |
300 | maas_ppas: list[str], |
301 | ) -> None: |
302 | """Add MAAS PPA to the given container.""" |
303 | for ppa in maas_ppas: |
304 | - exec_on_container( |
305 | + instance.execute( |
306 | ["add-apt-repository", "-y", ppa], |
307 | environment={"DEBIAN_FRONTEND": "noninteractive"}, |
308 | ) |
309 | |
310 | |
311 | @pytest.fixture(scope="session") |
312 | -def build_container(config: dict[str, Any]) -> Iterator[str]: |
313 | +def build_container(config: dict[str, Any]) -> Iterator[Instance]: |
314 | """Create a container for building MAAS package in.""" |
315 | log = getLogger(f"{LOG_NAME}.build_container") |
316 | lxd = get_lxd(log) |
317 | container_name = os.environ.get( |
318 | "MAAS_SYSTEMTESTS_BUILD_CONTAINER", "maas-system-build" |
319 | ) |
320 | - if not lxd.container_exists(container_name): |
321 | + instance = Instance(lxd, container_name) |
322 | + if not instance.exists(): |
323 | cloud_config = {} |
324 | |
325 | http_proxy = config.get("proxy", {}).get("http", "") |
326 | @@ -68,19 +64,19 @@ def build_container(config: dict[str, Any]) -> Iterator[str]: |
327 | profile=LXD_PROFILE, |
328 | ) |
329 | |
330 | - yield container_name |
331 | + yield instance |
332 | |
333 | |
334 | @pytest.fixture(scope="session") |
335 | def maas_deb_repo( |
336 | - build_container: str, config: dict[str, Any] |
337 | + build_container: Instance, config: dict[str, Any] |
338 | ) -> Iterator[Optional[str]]: |
339 | """Build maas deb, and setup APT repo.""" |
340 | if "snap" in config: |
341 | yield None |
342 | else: |
343 | - lxd = get_lxd(getLogger(f"{LOG_NAME}.maas_deb_repo")) |
344 | - build_ip = lxd.get_ip_address(build_container) |
345 | + build_container.logger = getLogger(f"{LOG_NAME}.maas_deb_repo") |
346 | + build_ip = build_container.get_ip_address() |
347 | http_proxy = config.get("proxy", {}).get("http", "") |
348 | proxy_env: Optional[dict[str, str]] |
349 | if http_proxy: |
350 | @@ -92,14 +88,12 @@ def maas_deb_repo( |
351 | else: |
352 | proxy_env = None |
353 | |
354 | - if not lxd.file_exists(build_container, "/var/www/html/repo/Packages.gz"): |
355 | + if not build_container.files["/var/www/html/repo/Packages.gz"].exists(): |
356 | maas_ppas = config.get("deb", {}).get( |
357 | "ppa", ["ppa:maas-committers/latest-deps"] |
358 | ) |
359 | - exec_on_container = partial(lxd.execute, build_container) |
360 | - _add_maas_ppa(exec_on_container, maas_ppas) |
361 | - lxd.execute( |
362 | - build_container, |
363 | + _add_maas_ppa(build_container, maas_ppas) |
364 | + build_container.execute( |
365 | [ |
366 | "apt", |
367 | "install", |
368 | @@ -116,8 +110,7 @@ def maas_deb_repo( |
369 | "git_repo", "https://git.launchpad.net/maas" |
370 | ) |
371 | maas_git_branch = str(config.get("deb", {}).get("git_branch", "master")) |
372 | - lxd.execute( |
373 | - build_container, |
374 | + build_container.execute( |
375 | [ |
376 | "git", |
377 | "clone", |
378 | @@ -134,8 +127,7 @@ def maas_deb_repo( |
379 | ], |
380 | environment=proxy_env, |
381 | ) |
382 | - lxd.execute( |
383 | - build_container, |
384 | + build_container.execute( |
385 | [ |
386 | "mk-build-deps", |
387 | "--install", |
388 | @@ -147,20 +139,18 @@ def maas_deb_repo( |
389 | environment={"DEBIAN_FRONTEND": "noninteractive"}, |
390 | ) |
391 | |
392 | - lxd.execute( |
393 | - build_container, |
394 | + build_container.execute( |
395 | ["make", "-C", "maas", "package"], |
396 | environment=proxy_env, |
397 | ) |
398 | - lxd.execute( |
399 | - build_container, |
400 | + build_container.execute( |
401 | [ |
402 | "sh", |
403 | "-c", |
404 | "cd build-area && (dpkg-scanpackages . | gzip -c > Packages.gz)", |
405 | ], |
406 | ) |
407 | - lxd.execute(build_container, ["mv", "build-area", "/var/www/html/repo"]) |
408 | + build_container.execute(["mv", "build-area", "/var/www/html/repo"]) |
409 | yield f"http://{build_ip}/repo" |
410 | |
411 | |
412 | @@ -191,29 +181,32 @@ def get_user_data( |
413 | |
414 | |
415 | @pytest.fixture(scope="session") |
416 | -def maas_container(config: dict[str, Any], build_container: str) -> str: |
417 | +def maas_container(config: dict[str, Any], build_container: Instance) -> Instance: |
418 | """Build a container for running MAAS in.""" |
419 | lxd = get_lxd(getLogger(f"{LOG_NAME}.maas_container")) |
420 | - container_name = os.environ.get( |
421 | + profile_name = container_name = os.environ.get( |
422 | "MAAS_SYSTEMTESTS_MAAS_CONTAINER", "maas-system-maas" |
423 | ) |
424 | - if not lxd.container_exists(container_name): |
425 | - if not lxd.profile_exists(container_name): |
426 | - lxd.copy_profile(LXD_PROFILE, container_name) |
427 | + instance = Instance(lxd, container_name) |
428 | + # FIXME: loggers could be a stack? |
429 | + build_container.logger = instance.logger = lxd.logger |
430 | + if not instance.exists(): |
431 | + if not lxd.profile_exists(profile_name): |
432 | + lxd.copy_profile(LXD_PROFILE, profile_name) |
433 | existing_maas_nics = [ |
434 | device_name |
435 | - for device_name in lxd.list_profile_devices(container_name) |
436 | + for device_name in lxd.list_profile_devices(profile_name) |
437 | if device_name.startswith("maas-ss-") |
438 | ] |
439 | for device_name in existing_maas_nics: |
440 | - lxd.remove_profile_device(container_name, device_name) |
441 | + lxd.remove_profile_device(profile_name, device_name) |
442 | maas_networks = config["maas"]["networks"] |
443 | devices = {} |
444 | for network_name, ip in maas_networks.items(): |
445 | network = config["networks"][network_name] |
446 | device_name = "maas-ss-" + network_name |
447 | lxd.add_profile_device( |
448 | - container_name, |
449 | + profile_name, |
450 | device_name, |
451 | "nic", |
452 | "name=" + device_name, |
453 | @@ -257,7 +250,7 @@ def maas_container(config: dict[str, Any], build_container: str) -> str: |
454 | ) |
455 | |
456 | if "snap" not in config: |
457 | - build_container_ip = lxd.get_ip_address(build_container) |
458 | + build_container_ip = build_container.get_ip_address() |
459 | contents = dedent( |
460 | f"""\ |
461 | Package: * |
462 | @@ -265,9 +258,7 @@ def maas_container(config: dict[str, Any], build_container: str) -> str: |
463 | Pin-Priority: 999 |
464 | """ |
465 | ) |
466 | - lxd.push_text_file( |
467 | - container_name, contents, "/etc/apt/preferences.d/maas-build-pin-999" |
468 | - ) |
469 | + instance.files["/etc/apt/preferences.d/maas-build-pin-999"].write(contents) |
470 | |
471 | http_proxy = config.get("proxy", {}).get("http", "") |
472 | if http_proxy: |
473 | @@ -279,27 +270,21 @@ def maas_container(config: dict[str, Any], build_container: str) -> str: |
474 | Acquire::https::Proxy::{build_container_ip} "DIRECT"; |
475 | """ |
476 | ) |
477 | - lxd.push_text_file( |
478 | - container_name, contents, "/etc/apt/apt.conf.d/80maas-system-test" |
479 | - ) |
480 | + instance.files["/etc/apt/apt.conf.d/80maas-system-test"].write(contents) |
481 | |
482 | - return container_name |
483 | + return instance |
484 | |
485 | |
486 | @pytest.fixture(scope="session") |
487 | -def vault(maas_container: str, config: dict[str, Any]) -> Optional[Vault]: |
488 | +def vault(maas_container: Instance, config: dict[str, Any]) -> Optional[Vault]: |
489 | snap_channel = config.get("vault", {}).get("snap-channel") |
490 | if not snap_channel: |
491 | return None |
492 | |
493 | - vault_logger = getLogger(f"{LOG_NAME}.vault") |
494 | - lxd = get_lxd(vault_logger) |
495 | - lxd.execute(maas_container, ["apt", "install", "--yes", "ssl-cert"]) |
496 | - lxd.execute( |
497 | - maas_container, ["snap", "install", "vault", f"--channel={snap_channel}"] |
498 | - ) |
499 | - lxd.execute( |
500 | - maas_container, |
501 | + vault_logger = maas_container.logger = getLogger(f"{LOG_NAME}.vault") |
502 | + maas_container.execute(["apt", "install", "--yes", "ssl-cert"]) |
503 | + maas_container.execute(["snap", "install", "vault", f"--channel={snap_channel}"]) |
504 | + maas_container.execute( |
505 | [ |
506 | "cp", |
507 | "/etc/ssl/certs/ssl-cert-snakeoil.pem", |
508 | @@ -326,7 +311,7 @@ def vault(maas_container: str, config: dict[str, Any]) -> Optional[Vault]: |
509 | """ |
510 | ) |
511 | vault_config_file = "/var/snap/vault/common/vault.hcl" |
512 | - lxd.push_text_file(maas_container, vault_config, vault_config_file) |
513 | + maas_container.files[vault_config_file].write(vault_config) |
514 | |
515 | systemd_unit = dedent( |
516 | f"""\ |
517 | @@ -345,23 +330,20 @@ def vault(maas_container: str, config: dict[str, Any]) -> Optional[Vault]: |
518 | WantedBy=multi-user.target |
519 | """ |
520 | ) |
521 | - lxd.push_text_file( |
522 | - maas_container, systemd_unit, "/etc/systemd/system/vault.service" |
523 | - ) |
524 | - lxd.execute(maas_container, ["systemctl", "enable", "--now", "vault"]) |
525 | + maas_container.files["/etc/systemd/system/vault.service"].write(systemd_unit) |
526 | + maas_container.execute(["systemctl", "enable", "--now", "vault"]) |
527 | |
528 | vault = Vault( |
529 | container=maas_container, |
530 | secrets_mount="maas-secrets", |
531 | secrets_path="maas", |
532 | - lxd=lxd, |
533 | logger=vault_logger, |
534 | ) |
535 | try: |
536 | vault.ensure_initialized() |
537 | except VaultNotReadyError as e: |
538 | - journalctl = lxd.execute( |
539 | - maas_container, ["journalctl", "--unit=vault", "--no-pager"] |
540 | + journalctl = maas_container.execute( |
541 | + ["journalctl", "--unit=vault", "--no-pager"] |
542 | ) |
543 | vault_logger.exception(f"{e}\n{journalctl.stdout}") |
544 | raise |
545 | @@ -370,21 +352,18 @@ def vault(maas_container: str, config: dict[str, Any]) -> Optional[Vault]: |
546 | |
547 | |
548 | def install_deb( |
549 | - lxd: CLILXD, maas_container: str, maas_deb_repo: str, config: dict[str, Any] |
550 | + maas_container: Instance, maas_deb_repo: str, config: dict[str, Any] |
551 | ) -> str: |
552 | - on_maas_container = partial(lxd.execute, maas_container) |
553 | maas_ppas = config.get("deb", {}).get("ppa", ["ppa:maas-committers/latest-deps"]) |
554 | - lxd.push_text_file( |
555 | - maas_container, |
556 | - f"deb [trusted=yes] {maas_deb_repo} ./\n", |
557 | - "/etc/apt/sources.list.d/maas.list", |
558 | + maas_container.files["/etc/apt/sources.list.d/maas.list"].write( |
559 | + f"deb [trusted=yes] {maas_deb_repo} ./\n" |
560 | ) |
561 | - _add_maas_ppa(on_maas_container, maas_ppas) |
562 | - on_maas_container( |
563 | + _add_maas_ppa(maas_container, maas_ppas) |
564 | + maas_container.execute( |
565 | ["apt", "install", "--yes", "maas"], |
566 | environment={"DEBIAN_FRONTEND": "noninteractive"}, |
567 | ) |
568 | - policy = on_maas_container( |
569 | + policy = maas_container.execute( |
570 | ["apt-cache", "policy", "maas"], |
571 | environment={"DEBIAN_FRONTEND": "noninteractive"}, |
572 | ) # just to record which version is running. |
573 | @@ -397,21 +376,20 @@ def install_deb( |
574 | |
575 | @pytest.fixture(scope="session") |
576 | def maas_region( |
577 | - maas_container: str, |
578 | + maas_container: Instance, |
579 | maas_deb_repo: Optional[str], |
580 | vault: Optional[Vault], |
581 | config: dict[str, Any], |
582 | ) -> Iterator[MAASRegion]: |
583 | """Install MAAS region controller in the container.""" |
584 | lxd = get_lxd(getLogger(f"{LOG_NAME}.maas_region")) |
585 | - |
586 | - region_ip = lxd.get_ip_address(maas_container) |
587 | + maas_container.logger = lxd.logger |
588 | + region_ip = maas_container.get_ip_address() |
589 | installed_from_snap = "snap" in config |
590 | |
591 | # setup self-signed certs, and add them to the trusted list |
592 | - lxd.execute(maas_container, ["apt", "install", "--yes", "ssl-cert"]) |
593 | - lxd.execute( |
594 | - maas_container, |
595 | + maas_container.execute(["apt", "install", "--yes", "ssl-cert"]) |
596 | + maas_container.execute( |
597 | [ |
598 | "cp", |
599 | "-n", |
600 | @@ -419,26 +397,25 @@ def maas_region( |
601 | "/usr/share/ca-certificates/ssl-cert-snakeoil.crt", |
602 | ], |
603 | ) |
604 | - certs_list = lxd.get_file_contents(maas_container, "/etc/ca-certificates.conf") |
605 | + ca_certificates = maas_container.files["/etc/ca-certificates.conf"] |
606 | + certs_list = ca_certificates.read() |
607 | if "ssl-cert-snakeoil.crt" not in certs_list: |
608 | - certs_list += "ssl-cert-snakeoil.crt\n" |
609 | - lxd.push_text_file(maas_container, certs_list, "/etc/ca-certificates.conf") |
610 | - lxd.execute(maas_container, ["update-ca-certificates"]) |
611 | + ca_certificates.write(certs_list + "ssl-cert-snakeoil.crt\n") |
612 | + maas_container.execute(["update-ca-certificates"]) |
613 | |
614 | if installed_from_snap: |
615 | - maas_already_initialized = lxd.file_exists( |
616 | - maas_container, "/var/snap/maas/common/snap_mode" |
617 | - ) |
618 | - snap_list = lxd.execute( |
619 | - maas_container, ["snap", "list", "maas"] |
620 | - ) # just to record which version is running. |
621 | + |
622 | + maas_already_initialized = maas_container.files[ |
623 | + "/var/snap/maas/common/snap_mode" |
624 | + ].exists() |
625 | + # just to record which version is running. |
626 | + snap_list = maas_container.execute(["snap", "list", "maas"]) |
627 | try: |
628 | version = snap_list.stdout.split("\n")[1].split()[1] |
629 | except IndexError: |
630 | version = "" |
631 | if not maas_already_initialized: |
632 | - lxd.execute( |
633 | - maas_container, |
634 | + maas_container.execute( |
635 | [ |
636 | "maas", |
637 | "init", |
638 | @@ -450,9 +427,8 @@ def maas_region( |
639 | ], |
640 | ) |
641 | else: |
642 | - # TODO: bind the LXD to the maas_container |
643 | assert maas_deb_repo is not None |
644 | - version = install_deb(lxd, maas_container, maas_deb_repo, config) |
645 | + version = install_deb(maas_container, maas_deb_repo, config) |
646 | with open("version_under_test", "w") as fh: |
647 | fh.write(f"{version}\n") |
648 | |
649 | @@ -466,9 +442,9 @@ def maas_region( |
650 | region_host = region_ip |
651 | |
652 | if vault: |
653 | - setup_vault(vault, lxd, maas_container) |
654 | + setup_vault(vault, maas_container) |
655 | if "tls" in config: |
656 | - region_host, url = setup_tls(lxd, maas_container) |
657 | + region_host, url = setup_tls(maas_container) |
658 | region = MAASRegion( |
659 | url=url, |
660 | http_url=http_url, |
661 | @@ -495,20 +471,18 @@ def maas_region( |
662 | yaml.dump(credentials, fh) |
663 | |
664 | if o11y := config.get("o11y"): |
665 | - setup_o11y(o11y, lxd, maas_container, installed_from_snap) |
666 | + setup_o11y(o11y, maas_container, installed_from_snap) |
667 | yield region |
668 | |
669 | |
670 | @pytest.fixture(scope="session") |
671 | def unauthenticated_maas_api_client( |
672 | maas_credentials: dict[str, str], |
673 | - maas_client_container: str, |
674 | + maas_client_container: Instance, |
675 | ) -> UnauthenticatedMAASAPIClient: |
676 | """Get an UnauthenticatedMAASAPIClient for interacting with MAAS.""" |
677 | return UnauthenticatedMAASAPIClient( |
678 | - maas_credentials["region_url"], |
679 | - maas_client_container, |
680 | - get_lxd(getLogger()), |
681 | + maas_credentials["region_url"], maas_client_container |
682 | ) |
683 | |
684 | |
685 | @@ -616,40 +590,40 @@ def tag_all(authenticated_admin: AuthenticatedAPIClient) -> Iterator[str]: |
686 | @pytest.fixture(scope="session") |
687 | def maas_client_container( |
688 | maas_credentials: dict[str, str], config: dict[str, Any] |
689 | -) -> Iterator[str]: |
690 | +) -> Iterator[Instance]: |
691 | """Set up a new LXD container with maas installed (in order to use maas CLI).""" |
692 | |
693 | log = getLogger(f"{LOG_NAME}.client_container") |
694 | lxd = get_lxd(log) |
695 | container = os.environ.get("MAAS_SYSTEMTESTS_CLIENT_CONTAINER", "maas-client") |
696 | |
697 | - lxd.get_or_create(container, config["containers-image"], profile=LXD_PROFILE) |
698 | + instance = lxd.get_or_create( |
699 | + container, config["containers-image"], profile=LXD_PROFILE |
700 | + ) |
701 | snap_channel = maas_credentials.get("snap_channel", "latest/edge") |
702 | - lxd.execute(container, ["snap", "refresh", "snapd"]) |
703 | - lxd.execute(container, ["snap", "install", "maas", f"--channel={snap_channel}"]) |
704 | - lxd.execute(container, ["snap", "list", "maas"]) |
705 | + instance.execute(["snap", "refresh", "snapd"]) |
706 | + instance.execute(["snap", "install", "maas", f"--channel={snap_channel}"]) |
707 | + instance.execute(["snap", "list", "maas"]) |
708 | ensure_host_ip_mapping( |
709 | - lxd, container, maas_credentials["region_host"], maas_credentials["region_ip"] |
710 | + instance, maas_credentials["region_host"], maas_credentials["region_ip"] |
711 | ) |
712 | if "tls" in config: |
713 | - lxd.execute(container, ["mkdir", "-p", MAAS_CONTAINER_CERTS_PATH]) |
714 | - lxd.push_file( |
715 | - container, |
716 | - config["tls"]["cacerts"], |
717 | - f"{MAAS_CONTAINER_CERTS_PATH}cacerts.pem", |
718 | + instance.execute(["mkdir", "-p", MAAS_CONTAINER_CERTS_PATH]) |
719 | + instance.files[f"{MAAS_CONTAINER_CERTS_PATH}cacerts.pem"].push( |
720 | + config["tls"]["cacerts"] |
721 | ) |
722 | |
723 | - yield container |
724 | + yield instance |
725 | |
726 | |
727 | -def ensure_host_ip_mapping(lxd: CLILXD, container: str, hostname: str, ip: str) -> None: |
728 | +def ensure_host_ip_mapping(instance: Instance, hostname: str, ip: str) -> None: |
729 | """Ensure the /etc/hosts file contains the specified host/ip mapping.""" |
730 | if hostname == ip: |
731 | # no need to add the alias |
732 | return |
733 | line = f"{ip} {hostname}\n" |
734 | - content = lxd.get_file_contents(container, "/etc/hosts") |
735 | + hosts_file = instance.files["/etc/hosts"] |
736 | + content = hosts_file.read() |
737 | if line in content: |
738 | return |
739 | - content += line |
740 | - lxd.push_text_file(container, content, "/etc/hosts") |
741 | + hosts_file.write(content + line) |
742 | diff --git a/systemtests/lxd.py b/systemtests/lxd.py |
743 | index 72483f5..f660ffd 100644 |
744 | --- a/systemtests/lxd.py |
745 | +++ b/systemtests/lxd.py |
746 | @@ -36,6 +36,130 @@ class BadWebSocketHandshakeError(Exception): |
747 | """Raised when lxc execute gives a bad websocket handshake error.""" |
748 | |
749 | |
750 | +class _FileWrapper: |
751 | + def __init__(self, lxd: CLILXD, instance_name: str, path: str): |
752 | + self._lxd = lxd |
753 | + self._instance_name = instance_name |
754 | + self._path = path |
755 | + |
756 | + def __repr__(self) -> str: |
757 | + return f"<FileWrapper for {self._path} on {self._instance_name}>" |
758 | + |
759 | + def read(self) -> str: |
760 | + return self._lxd.get_file_contents(self._instance_name, self._path) |
761 | + |
762 | + def write(self, content: str, uid: int = 0, gid: int = 0) -> None: |
763 | + return self._lxd.push_text_file( |
764 | + self._instance_name, content, self._path, uid=uid, gid=gid |
765 | + ) |
766 | + |
767 | + def exists(self) -> bool: |
768 | + return self._lxd.file_exists(self._instance_name, self._path) |
769 | + |
770 | + def push( |
771 | + self, |
772 | + local_path: Path, |
773 | + uid: int = 0, |
774 | + gid: int = 0, |
775 | + mode: str = "", |
776 | + create_dirs: bool = False, |
777 | + ) -> None: |
778 | + return self._lxd.push_file( |
779 | + self._instance_name, |
780 | + str(local_path), |
781 | + self._path, |
782 | + uid=uid, |
783 | + gid=gid, |
784 | + mode=mode, |
785 | + create_dirs=create_dirs, |
786 | + ) |
787 | + |
788 | + def pull(self, local_path: str) -> None: |
789 | + self._lxd.pull_file(self._instance_name, self._path, local_path) |
790 | + |
791 | + |
792 | +class _FilesWrapper: |
793 | + def __init__(self, lxd: CLILXD, instance_name: str): |
794 | + super().__init__() |
795 | + self._lxd = lxd |
796 | + self._instance_name = instance_name |
797 | + |
798 | + def __getitem__(self, path: str) -> _FileWrapper: |
799 | + return _FileWrapper(self._lxd, self._instance_name, path) |
800 | + |
801 | + |
802 | +class Instance: |
803 | + """A container or VM on a LXD.""" |
804 | + |
805 | + def __init__(self, lxd: CLILXD, instance_name: str): |
806 | + self._lxd = lxd |
807 | + self._instance_name = instance_name |
808 | + |
809 | + def __repr__(self) -> str: |
810 | + return f"<Instance {self.name}>" |
811 | + |
812 | + @property |
813 | + def name(self) -> str: |
814 | + return self._instance_name |
815 | + |
816 | + @property |
817 | + def logger(self) -> logging.Logger: |
818 | + return self._lxd.logger |
819 | + |
820 | + @logger.setter |
821 | + def logger(self, logger: logging.Logger) -> None: |
822 | + self._lxd.logger = logger |
823 | + |
824 | + def exists(self) -> bool: |
825 | + return self._lxd.container_exists(self._instance_name) |
826 | + |
827 | + def execute( |
828 | + self, command: list[str], environment: Optional[dict[str, str]] = None |
829 | + ) -> subprocess.CompletedProcess[str]: |
830 | + return self._lxd.execute(self._instance_name, command, environment=environment) |
831 | + |
832 | + def quietly_execute( |
833 | + self, command: list[str], environment: Optional[dict[str, str]] = None |
834 | + ) -> subprocess.CompletedProcess[str]: |
835 | + return self._lxd.quietly_execute( |
836 | + self._instance_name, command, environment=environment |
837 | + ) |
838 | + |
839 | + @property |
840 | + def files(self) -> _FilesWrapper: |
841 | + return _FilesWrapper(self._lxd, self._instance_name) |
842 | + |
843 | + def get_ip_address(self) -> str: |
844 | + return self._lxd.get_ip_address(self._instance_name) |
845 | + |
846 | + def list_devices(self) -> list[str]: |
847 | + return self._lxd.list_instance_devices(self._instance_name) |
848 | + |
849 | + def add_device(self, name: str, device_config: DeviceConfig) -> None: |
850 | + return self._lxd.add_instance_device(self._instance_name, name, device_config) |
851 | + |
852 | + def remove_device(self, name: str) -> None: |
853 | + return self._lxd.remove_instance_device(self._instance_name, name) |
854 | + |
855 | + def start(self) -> subprocess.CompletedProcess[str]: |
856 | + return self._lxd.start(self._instance_name) |
857 | + |
858 | + def stop(self, force: bool = False) -> subprocess.CompletedProcess[str]: |
859 | + return self._lxd.stop(self._instance_name, force=force) |
860 | + |
861 | + def restart(self) -> subprocess.CompletedProcess[str]: |
862 | + return self._lxd.restart(self._instance_name) |
863 | + |
864 | + def delete(self) -> None: |
865 | + return self._lxd.delete(self._instance_name) |
866 | + |
867 | + def is_running(self) -> bool: |
868 | + return self._lxd.is_running(self._instance_name) |
869 | + |
870 | + def status(self) -> str: |
871 | + return self._lxd.instance_status(self._instance_name) |
872 | + |
873 | + |
874 | class CLILXD: |
875 | """Backend that uses the CLI to talk to LXD.""" |
876 | |
877 | @@ -68,7 +192,7 @@ class CLILXD: |
878 | image: str, |
879 | user_data: Optional[str] = None, |
880 | profile: Optional[str] = None, |
881 | - ) -> str: |
882 | + ) -> Instance: |
883 | logger = self.logger.getChild(name) |
884 | if not self.container_exists(name): |
885 | logger.info(f"Creating container {name} (from {image})...") |
886 | @@ -88,20 +212,22 @@ class CLILXD: |
887 | logger.info(f"Container {name} created.") |
888 | logger.info("Waiting for boot to finish...") |
889 | |
890 | + instance = Instance(self, name) |
891 | + |
892 | @retry(exceptions=CloudInitDisabled, tries=120, delay=1, logger=logger) |
893 | def _cloud_init_wait() -> None: |
894 | - process = self.execute( |
895 | - name, ["timeout", "2000", "cloud-init", "status", "--wait", "--long"] |
896 | + process = instance.execute( |
897 | + ["timeout", "2000", "cloud-init", "status", "--wait", "--long"] |
898 | ) |
899 | if "Cloud-init disabled by cloud-init-generator" in process.stdout: |
900 | raise CloudInitDisabled("Cloud-init is disabled.") |
901 | - process = self.execute( |
902 | - name, ["timeout", "2000", "snap", "wait", "system", "seed.loaded"] |
903 | + process = instance.execute( |
904 | + ["timeout", "2000", "snap", "wait", "system", "seed.loaded"] |
905 | ) |
906 | |
907 | _cloud_init_wait() |
908 | logger.info("Boot finished.") |
909 | - return name |
910 | + return instance |
911 | |
912 | def get_or_create( |
913 | self, |
914 | @@ -109,10 +235,11 @@ class CLILXD: |
915 | image: str, |
916 | user_data: Optional[str] = None, |
917 | profile: Optional[str] = None, |
918 | - ) -> str: |
919 | - if not self.container_exists(name): |
920 | + ) -> Instance: |
921 | + instance = Instance(self, name) |
922 | + if not instance.exists(): |
923 | self.create_container(name, image, user_data=user_data, profile=profile) |
924 | - return name |
925 | + return instance |
926 | |
927 | def push_file( |
928 | self, |
929 | @@ -325,27 +452,6 @@ class CLILXD: |
930 | ["lxc", "profile", "device", "remove", profile_name, device_name], |
931 | ) |
932 | |
933 | - def collect_sos_report(self, instance_name: str, output: str) -> None: |
934 | - container_tmp = "/tmp/sosreport" |
935 | - output_dir = Path(f"{output}/sosreport") |
936 | - self.execute(instance_name, ["apt", "install", "--yes", "sosreport"]) |
937 | - self.execute(instance_name, ["rm", "-rf", container_tmp]) |
938 | - self.execute(instance_name, ["mkdir", "-p", container_tmp]) |
939 | - self.execute( |
940 | - instance_name, |
941 | - ["sos", "report", "--batch", "-o", "maas", "--tmp-dir", container_tmp], |
942 | - ) |
943 | - output_dir.mkdir(parents=True, exist_ok=True) |
944 | - journalctl = self.execute( |
945 | - instance_name, ["journalctl", "--unit=vault", "--no-pager", "--output=cat"] |
946 | - ) |
947 | - if journalctl.stdout: |
948 | - (output_dir / "vault-journal").write_text(journalctl.stdout) |
949 | - with tempfile.TemporaryDirectory(prefix="sosreport") as tempdir: |
950 | - self.pull_file(instance_name, container_tmp, f"{tempdir}/") |
951 | - for f in os.listdir(f"{tempdir}/sosreport"): |
952 | - os.rename(os.path.join(f"{tempdir}/sosreport", f), f"{output_dir}/{f}") |
953 | - |
954 | def list_instance_devices(self, instance_name: str) -> list[str]: |
955 | logger = self.logger.getChild(instance_name) |
956 | result = self._run( |
957 | @@ -395,7 +501,7 @@ class CLILXD: |
958 | instances = {instance["name"]: instance for instance in lxc_list} |
959 | return instances |
960 | |
961 | - def create_vm(self, instance_name: str, config: dict[str, str]) -> None: |
962 | + def create_vm(self, instance_name: str, config: dict[str, str]) -> Instance: |
963 | logger = self.logger.getChild(instance_name) |
964 | args: list[str] = [] |
965 | profile: Optional[str] = config.pop("profile", None) |
966 | @@ -417,6 +523,7 @@ class CLILXD: |
967 | ], |
968 | logger=logger, |
969 | ) |
970 | + return Instance(self, instance_name) |
971 | |
972 | def start(self, instance_name: str) -> subprocess.CompletedProcess[str]: |
973 | logger = self.logger.getChild(instance_name) |
974 | diff --git a/systemtests/o11y.py b/systemtests/o11y.py |
975 | index a5366ff..4dd4342 100644 |
976 | --- a/systemtests/o11y.py |
977 | +++ b/systemtests/o11y.py |
978 | @@ -1,28 +1,24 @@ |
979 | +from pathlib import Path |
980 | from textwrap import dedent |
981 | from typing import Any |
982 | |
983 | -from .lxd import CLILXD |
984 | +from .lxd import Instance |
985 | |
986 | AGENT_PATH = "/opt/agent/agent-linux-amd64" |
987 | |
988 | |
989 | def setup_o11y( |
990 | - o11y: dict[str, Any], lxd: CLILXD, maas_container: str, installed_from_snap: bool |
991 | + o11y: dict[str, Any], maas_container: Instance, installed_from_snap: bool |
992 | ) -> None: |
993 | - if not lxd.file_exists(maas_container, AGENT_PATH): |
994 | - host_path_to_agent = o11y["grafana_agent_file_path"].strip() |
995 | - lxd.execute(maas_container, ["mkdir", "-p", "/opt/agent"]) |
996 | - lxd.push_file( |
997 | - maas_container, |
998 | - host_path_to_agent, |
999 | - AGENT_PATH, |
1000 | - mode="0755", |
1001 | - ) |
1002 | + agent_on_container = maas_container.files[AGENT_PATH] |
1003 | + if not agent_on_container.exists(): |
1004 | + host_path_to_agent = Path(o11y["grafana_agent_file_path"].strip()) |
1005 | + maas_container.execute(["mkdir", "-p", "/opt/agent"]) |
1006 | + agent_on_container.push(host_path_to_agent, mode="0755") |
1007 | agent_maas_sample = "/usr/share/maas/grafana_agent/agent.yaml.example" |
1008 | if installed_from_snap: |
1009 | agent_maas_sample = f"/snap/maas/current{agent_maas_sample}" |
1010 | - lxd.execute( |
1011 | - maas_container, |
1012 | + maas_container.execute( |
1013 | ["cp", agent_maas_sample, "/opt/agent/agent.yml"], |
1014 | ) |
1015 | o11y_ip = o11y["o11y_ip"] |
1016 | @@ -47,4 +43,4 @@ def setup_o11y( |
1017 | -server.http.address="0.0.0.0:3100" -server.grpc.address="0.0.0.0:9095" |
1018 | """ |
1019 | ) |
1020 | - lxd.execute(maas_container, ["sh", "-c", telemetry_run_cmd]) |
1021 | + maas_container.execute(["sh", "-c", telemetry_run_cmd]) |
1022 | diff --git a/systemtests/region.py b/systemtests/region.py |
1023 | index 014cbc8..4b979b6 100644 |
1024 | --- a/systemtests/region.py |
1025 | +++ b/systemtests/region.py |
1026 | @@ -4,11 +4,11 @@ import subprocess |
1027 | from logging import getLogger |
1028 | from typing import TYPE_CHECKING, Any, Union |
1029 | |
1030 | -from .lxd import get_lxd |
1031 | from .utils import retries |
1032 | |
1033 | if TYPE_CHECKING: |
1034 | from .api import AuthenticatedAPIClient |
1035 | + from .lxd import Instance |
1036 | |
1037 | LOG = getLogger("systemtests.region") |
1038 | |
1039 | @@ -19,15 +19,15 @@ class MAASRegion: |
1040 | url: str, |
1041 | http_url: str, |
1042 | host: str, |
1043 | - maas_container: str, |
1044 | + maas_container: Instance, |
1045 | installed_from_snap: bool, |
1046 | ): |
1047 | self.url = url |
1048 | self.http_url = http_url |
1049 | self.host = host |
1050 | self.maas_container = maas_container |
1051 | + self.maas_container.logger = LOG |
1052 | self.installed_from_snap = installed_from_snap |
1053 | - self.lxd = get_lxd(LOG) |
1054 | |
1055 | def __repr__(self) -> str: |
1056 | package = "snap" if self.installed_from_snap else "deb" |
1057 | @@ -36,7 +36,7 @@ class MAASRegion: |
1058 | ) |
1059 | |
1060 | def execute(self, command: list[str]) -> subprocess.CompletedProcess[str]: |
1061 | - return self.lxd.execute(self.maas_container, command) |
1062 | + return self.maas_container.execute(command) |
1063 | |
1064 | def get_api_token(self, user: str) -> str: |
1065 | result = self.execute(["maas", "apikey", "--username", user]) |
1066 | @@ -158,7 +158,7 @@ class MAASRegion: |
1067 | dhcpd_conf_path = "/var/lib/maas/dhcpd.conf" |
1068 | if self.installed_from_snap: |
1069 | dhcpd_conf_path = "/var/snap/maas/common/maas/dhcpd.conf" |
1070 | - return self.lxd.file_exists(self.maas_container, dhcpd_conf_path) |
1071 | + return self.maas_container.files[dhcpd_conf_path].exists() |
1072 | |
1073 | def set_config(self, key: str, value: str = "") -> None: |
1074 | if self.installed_from_snap: |
1075 | diff --git a/systemtests/state.py b/systemtests/state.py |
1076 | index b79b247..57f0f7c 100644 |
1077 | --- a/systemtests/state.py |
1078 | +++ b/systemtests/state.py |
1079 | @@ -3,7 +3,8 @@ from __future__ import annotations |
1080 | import json |
1081 | import time |
1082 | from logging import getLogger |
1083 | -from typing import TYPE_CHECKING, Any, Iterator, Set, cast |
1084 | +from pathlib import Path |
1085 | +from typing import TYPE_CHECKING, Any, Iterator, Set |
1086 | |
1087 | import pytest |
1088 | from retry import retry |
1089 | @@ -75,11 +76,8 @@ def import_images_and_wait_until_synced( |
1090 | if "windows" in osystems: |
1091 | windows_path = "/home/ubuntu/windows-win2012hvr2-amd64-root-dd" |
1092 | # 1000/1000 is default uid/gid of ubuntu user |
1093 | - authenticated_admin.api_client.push_file( |
1094 | - source_file=config["windows_image_file_path"], |
1095 | - target_file=windows_path, |
1096 | - uid=1000, |
1097 | - gid=1000, |
1098 | + authenticated_admin.api_client.maas_container.files[windows_path].push( |
1099 | + Path(config["windows_image_file_path"]), uid=1000, gid=1000 |
1100 | ) |
1101 | |
1102 | region_start_point = time.time() |
1103 | @@ -94,7 +92,7 @@ def import_images_and_wait_until_synced( |
1104 | ) |
1105 | if "windows" in osystems: |
1106 | windows_start_point = time.time() |
1107 | - windows_path = cast(str, windows_path) |
1108 | + assert windows_path is not None |
1109 | authenticated_admin.create_boot_resource( |
1110 | name="windows/win2012hvr2", |
1111 | title="Windows2012HVR2", |
1112 | diff --git a/systemtests/tests_per_machine/test_hardware_sync.py b/systemtests/tests_per_machine/test_hardware_sync.py |
1113 | index 05533c3..1bab2c4 100644 |
1114 | --- a/systemtests/tests_per_machine/test_hardware_sync.py |
1115 | +++ b/systemtests/tests_per_machine/test_hardware_sync.py |
1116 | @@ -11,7 +11,6 @@ from pytest_steps.steps_generator import optional_step |
1117 | from retry.api import retry, retry_call |
1118 | |
1119 | from ..device_config import DeviceConfig, HardwareSyncMachine |
1120 | -from ..lxd import get_lxd |
1121 | from ..utils import ( |
1122 | retries, |
1123 | ssh_execute_command, |
1124 | @@ -25,15 +24,7 @@ if TYPE_CHECKING: |
1125 | from paramiko import PKey |
1126 | |
1127 | from ..api import AuthenticatedAPIClient, Machine |
1128 | - from ..lxd import CLILXD |
1129 | - |
1130 | - |
1131 | -def assert_device_not_added_to_lxd_instance( |
1132 | - lxd: CLILXD, |
1133 | - machine_name: str, |
1134 | - device_config: DeviceConfig, |
1135 | -) -> None: |
1136 | - assert device_config["device_name"] not in lxd.list_instance_devices(machine_name) |
1137 | + from ..lxd import Instance |
1138 | |
1139 | |
1140 | def _check_machine_for_device( |
1141 | @@ -79,27 +70,27 @@ def check_machine_does_not_have_device( |
1142 | |
1143 | |
1144 | @contextmanager |
1145 | -def powered_off_vm(lxd: CLILXD, instance_name: str) -> Iterator[None]: |
1146 | +def powered_off_vm(instance: Instance) -> Iterator[None]: |
1147 | """Context-manager to do something with LXD instance off.""" |
1148 | - strategy_iter: Iterator[Callable[[str], CompletedProcess[str]]] = chain( |
1149 | - repeat(lxd.stop, 3), repeat(partial(lxd.stop, force=True)) |
1150 | + strategy_iter: Iterator[Callable[[], CompletedProcess[str]]] = chain( |
1151 | + repeat(instance.stop, 3), repeat(partial(instance.stop, force=True)) |
1152 | ) |
1153 | |
1154 | - @retry(tries=10, delay=5, logger=lxd.logger) |
1155 | - def power_off(instance_name: str) -> None: |
1156 | - if lxd.is_running(instance_name): |
1157 | + @retry(tries=10, delay=5, logger=instance.logger) |
1158 | + def power_off(instance: Instance) -> None: |
1159 | + if instance.is_running(): |
1160 | stop_attempt = next(strategy_iter) |
1161 | - stop_attempt(instance_name) |
1162 | - if lxd.is_running(instance_name): |
1163 | - raise Exception(f"LXD {instance_name} still running.") |
1164 | + stop_attempt() |
1165 | + if instance.is_running(): |
1166 | + raise Exception(f"{instance.name} still running.") |
1167 | |
1168 | - @retry(tries=10, delay=5, backoff=1.2, logger=lxd.logger) |
1169 | - def power_on(instance_name: str) -> None: |
1170 | - lxd.start(instance_name) |
1171 | + @retry(tries=10, delay=5, backoff=1.2, logger=instance.logger) |
1172 | + def power_on(instance: Instance) -> None: |
1173 | + instance.start() |
1174 | |
1175 | - power_off(instance_name) |
1176 | + power_off(instance) |
1177 | yield |
1178 | - power_on(instance_name) |
1179 | + power_on(instance) |
1180 | |
1181 | |
1182 | @test_steps( |
1183 | @@ -117,8 +108,7 @@ def test_hardware_sync( |
1184 | ssh_key: PKey, |
1185 | testlog: Logger, |
1186 | ) -> Iterator[None]: |
1187 | - lxd = get_lxd(logger=testlog) |
1188 | - |
1189 | + hardware_sync_machine.instance.logger = testlog |
1190 | maas_api_client.set_config("hardware_sync_interval", "5s") |
1191 | |
1192 | maas_api_client.deploy_machine( |
1193 | @@ -144,21 +134,19 @@ def test_hardware_sync( |
1194 | testlog.info(f"{hardware_sync_machine.name}: {stdout}") |
1195 | |
1196 | yield |
1197 | - |
1198 | + instance = hardware_sync_machine.instance |
1199 | with optional_step("add_device") as add_device: |
1200 | # we don't have a way of remotely adding physical hardware, |
1201 | # so this test only tests lxd instances |
1202 | assert hardware_sync_machine.machine["power_type"] == "lxd" |
1203 | |
1204 | + current_devices = instance.list_devices() |
1205 | for device_config in hardware_sync_machine.devices_config: |
1206 | - assert_device_not_added_to_lxd_instance( |
1207 | - lxd, hardware_sync_machine.name, device_config |
1208 | - ) |
1209 | + assert device_config["device_name"] not in current_devices |
1210 | |
1211 | - with powered_off_vm(lxd, hardware_sync_machine.name): |
1212 | + with powered_off_vm(instance): |
1213 | for device_config in hardware_sync_machine.devices_config: |
1214 | - lxd.add_instance_device( |
1215 | - hardware_sync_machine.name, |
1216 | + instance.add_device( |
1217 | device_config["device_name"], |
1218 | device_config, |
1219 | ) |
1220 | @@ -220,11 +208,9 @@ def test_hardware_sync( |
1221 | |
1222 | with optional_step("remove_device", depends_on=add_device) as remove_device: |
1223 | if remove_device.should_run(): |
1224 | - with powered_off_vm(lxd, hardware_sync_machine.name): |
1225 | + with powered_off_vm(instance): |
1226 | for device_config in hardware_sync_machine.devices_config: |
1227 | - lxd.remove_instance_device( |
1228 | - hardware_sync_machine.name, device_config["device_name"] |
1229 | - ) |
1230 | + instance.remove_device(device_config["device_name"]) |
1231 | |
1232 | for device_config in hardware_sync_machine.devices_config: |
1233 | retry_call( |
1234 | @@ -248,5 +234,5 @@ def test_hardware_sync( |
1235 | break |
1236 | elif power_state == "on": |
1237 | with suppress(CalledProcessError): |
1238 | - lxd.stop(hardware_sync_machine.name, force=True) |
1239 | + instance.stop(force=True) |
1240 | yield |
1241 | diff --git a/systemtests/tls.py b/systemtests/tls.py |
1242 | index 47e7cb4..66cbdb4 100644 |
1243 | --- a/systemtests/tls.py |
1244 | +++ b/systemtests/tls.py |
1245 | @@ -1,14 +1,13 @@ |
1246 | -from .lxd import CLILXD |
1247 | +from .lxd import Instance |
1248 | |
1249 | # Certs must be accessible for MAAS installed by snap, but |
1250 | # this location is useful also when installed via deb package. |
1251 | MAAS_CONTAINER_CERTS_PATH = "/var/snap/maas/common/certs/" |
1252 | |
1253 | |
1254 | -def setup_tls(lxd: CLILXD, maas_container: str) -> tuple[str, str]: |
1255 | - lxd.execute(maas_container, ["mkdir", "-p", MAAS_CONTAINER_CERTS_PATH]) |
1256 | - lxd.execute( |
1257 | - maas_container, |
1258 | +def setup_tls(maas_container: Instance) -> tuple[str, str]: |
1259 | + maas_container.execute(["mkdir", "-p", MAAS_CONTAINER_CERTS_PATH]) |
1260 | + maas_container.execute( |
1261 | [ |
1262 | "cp", |
1263 | "-n", |
1264 | @@ -18,13 +17,10 @@ def setup_tls(lxd: CLILXD, maas_container: str) -> tuple[str, str]: |
1265 | ], |
1266 | ) |
1267 | # We need the cert to add it as CA in client container. |
1268 | - lxd.pull_file( |
1269 | - maas_container, |
1270 | - "/etc/ssl/certs/ssl-cert-snakeoil.pem", |
1271 | - "ssl-cert-snakeoil.pem", |
1272 | + maas_container.files["/etc/ssl/certs/ssl-cert-snakeoil.pem"].pull( |
1273 | + "ssl-cert-snakeoil.pem" |
1274 | ) |
1275 | - lxd.execute( |
1276 | - maas_container, |
1277 | + maas_container.execute( |
1278 | [ |
1279 | "maas", |
1280 | "config-tls", |
1281 | @@ -36,5 +32,5 @@ def setup_tls(lxd: CLILXD, maas_container: str) -> tuple[str, str]: |
1282 | "--yes", |
1283 | ], |
1284 | ) |
1285 | - region_host = lxd.quietly_execute(maas_container, ["hostname", "-f"]).stdout.strip() |
1286 | + region_host = maas_container.quietly_execute(["hostname", "-f"]).stdout.strip() |
1287 | return region_host, f"https://{region_host}:5443/MAAS/" |
1288 | diff --git a/systemtests/utils.py b/systemtests/utils.py |
1289 | index d3858bd..1fe7535 100644 |
1290 | --- a/systemtests/utils.py |
1291 | +++ b/systemtests/utils.py |
1292 | @@ -7,12 +7,14 @@ import re |
1293 | import string |
1294 | import time |
1295 | from dataclasses import dataclass |
1296 | +from logging import Logger |
1297 | from typing import Iterator, Optional, TypedDict, Union |
1298 | |
1299 | import paramiko |
1300 | from retry.api import retry_call |
1301 | |
1302 | from . import api |
1303 | +from .lxd import get_lxd |
1304 | |
1305 | |
1306 | class UnexpectedMachineStatus(Exception): |
1307 | @@ -184,6 +186,15 @@ def wait_for_machine( |
1308 | raise UnexpectedMachineStatus(machine_id, status, retry_info.elapsed, debug_outputs) |
1309 | |
1310 | |
1311 | +def debug_lxd_vm(machine_name: str, logger: Logger) -> list[str]: |
1312 | + """Grab debug information for a VM failure.""" |
1313 | + lxd = get_lxd(logger) |
1314 | + debug_info = [] |
1315 | + debug_info.append(repr(lxd.list_instances().get(machine_name, ""))) |
1316 | + debug_info.append(lxd._run(["lxc", "info", "--show-log", machine_name]).stdout) |
1317 | + return debug_info |
1318 | + |
1319 | + |
1320 | # XXX: Move to api.py |
1321 | def wait_for_new_machine( |
1322 | api_client: api.AuthenticatedAPIClient, mac_address: str, machine_name: str |
1323 | @@ -200,8 +211,8 @@ def wait_for_new_machine( |
1324 | debug_outputs = [] |
1325 | maybe_machines = quiet_client.list_machines(mac_address=mac_address) |
1326 | debug_outputs.append(repr(maybe_machines)) |
1327 | - if "lxd" in [m["power_type"] for m in machines]: |
1328 | - debug_outputs.append(repr(api_client.lxd.list_instances().get(machine_name))) |
1329 | + if "lxd" in [m["power_type"] for m in maybe_machines]: |
1330 | + debug_outputs.extend(debug_lxd_vm(machine_name, api_client.logger)) |
1331 | |
1332 | raise UnexpectedMachineStatus( |
1333 | machine_name, "New", retry_info.elapsed, debug_outputs |
1334 | @@ -226,7 +237,7 @@ def wait_for_machine_to_power_off( |
1335 | |
1336 | debug_outputs = [repr(machine)] |
1337 | if machine["power_type"] == "lxd": |
1338 | - debug_outputs.append(repr(api_client.lxd.list_instances()[machine_name])) |
1339 | + debug_outputs.extend(debug_lxd_vm(machine_name, api_client.logger)) |
1340 | raise UnexpectedMachineStatus( |
1341 | machine_name, "power_off", retry_info.elapsed, debug_outputs |
1342 | ) |
1343 | diff --git a/systemtests/vault.py b/systemtests/vault.py |
1344 | index 85f86ca..178ea46 100644 |
1345 | --- a/systemtests/vault.py |
1346 | +++ b/systemtests/vault.py |
1347 | @@ -10,7 +10,7 @@ from typing import Any, cast |
1348 | import yaml |
1349 | from retry.api import retry_call |
1350 | |
1351 | -from .lxd import CLILXD |
1352 | +from .lxd import Instance |
1353 | |
1354 | |
1355 | class VaultNotReadyError(Exception): |
1356 | @@ -21,10 +21,9 @@ class VaultNotReadyError(Exception): |
1357 | class Vault: |
1358 | """Vault CLI wrapper to be run inside a container.""" |
1359 | |
1360 | - container: str |
1361 | + container: Instance |
1362 | secrets_path: str |
1363 | secrets_mount: str |
1364 | - lxd: CLILXD |
1365 | logger: Logger |
1366 | root_token: str = "" |
1367 | |
1368 | @@ -34,9 +33,7 @@ class Vault: |
1369 | @cached_property |
1370 | def addr(self) -> str: |
1371 | """The Vault address.""" |
1372 | - fqdn = self.lxd.quietly_execute( |
1373 | - self.container, ["hostname", "-f"] |
1374 | - ).stdout.strip() |
1375 | + fqdn = self.container.quietly_execute(["hostname", "-f"]).stdout.strip() |
1376 | return f"https://{fqdn}:8200" |
1377 | |
1378 | def wait_ready(self) -> dict[str, Any]: |
1379 | @@ -61,8 +58,8 @@ class Vault: |
1380 | return cast( |
1381 | dict[str, Any], |
1382 | json.loads( |
1383 | - self.lxd.quietly_execute( |
1384 | - self.container, ["curl", f"{self.addr}/v1/sys/seal-status"] |
1385 | + self.container.quietly_execute( |
1386 | + ["curl", f"{self.addr}/v1/sys/seal-status"] |
1387 | ).stdout |
1388 | ), |
1389 | ) |
1390 | @@ -79,8 +76,7 @@ class Vault: |
1391 | environment = {"VAULT_ADDR": self.addr} |
1392 | if self.root_token: |
1393 | environment["VAULT_TOKEN"] = self.root_token |
1394 | - return self.lxd.quietly_execute( |
1395 | - self.container, |
1396 | + return self.container.quietly_execute( |
1397 | ["vault"] + list(command), |
1398 | environment=environment, |
1399 | ) |
1400 | @@ -95,17 +91,13 @@ class Vault: |
1401 | """Ensure Vault is initialized and unlocked.""" |
1402 | status = self.wait_ready() |
1403 | |
1404 | - vault_init_file = "/var/snap/vault/common/init.json" |
1405 | + vault_init_file = self.container.files["/var/snap/vault/common/init.json"] |
1406 | if status["initialized"]: |
1407 | - init_result = json.loads( |
1408 | - self.lxd.get_file_contents(self.container, vault_init_file) |
1409 | - ) |
1410 | + init_result = json.loads(vault_init_file.read()) |
1411 | self.root_token = init_result["root_token"] |
1412 | else: |
1413 | init_result = self.init() |
1414 | - self.lxd.push_text_file( |
1415 | - self.container, json.dumps(init_result), vault_init_file |
1416 | - ) |
1417 | + vault_init_file.write(json.dumps(init_result)) |
1418 | |
1419 | while (status := self.status())["sealed"]: |
1420 | index = status["progress"] |
1421 | @@ -141,9 +133,9 @@ class Vault: |
1422 | """ |
1423 | ) |
1424 | tmpfile = f"/root/vault-policy-{uuid.uuid4()}" |
1425 | - self.lxd.push_text_file(self.container, policy, tmpfile) |
1426 | + self.container.files[tmpfile].write(policy) |
1427 | self.execute("policy", "write", self.MAAS_POLICY_NAME, tmpfile) |
1428 | - self.lxd.quietly_execute(self.container, ["rm", tmpfile]) |
1429 | + self.container.quietly_execute(["rm", tmpfile]) |
1430 | |
1431 | def create_approle(self, role_name: str) -> tuple[str, str]: |
1432 | """Create an approle with secret and return its ID and wrapped token.""" |
1433 | @@ -165,17 +157,16 @@ class Vault: |
1434 | return role_id, wrapped_token |
1435 | |
1436 | |
1437 | -def setup_vault(vault: Vault, lxd: CLILXD, maas_container: str) -> None: |
1438 | +def setup_vault(vault: Vault, maas_container: Instance) -> None: |
1439 | """Configures MAAS to talk to Vault.""" |
1440 | maas_vault_status = yaml.safe_load( |
1441 | - lxd.quietly_execute( |
1442 | - maas_container, ["maas", "config-vault", "status"] |
1443 | + maas_container.quietly_execute( |
1444 | + ["maas", "config-vault", "status"] |
1445 | ).stdout.strip() |
1446 | ) |
1447 | if maas_vault_status["status"] == "disabled": |
1448 | - role_id, wrapped_token = vault.create_approle(maas_container) |
1449 | - lxd.execute( |
1450 | - maas_container, |
1451 | + role_id, wrapped_token = vault.create_approle(maas_container.name) |
1452 | + maas_container.execute( |
1453 | [ |
1454 | "maas", |
1455 | "config-vault", |
1456 | @@ -188,8 +179,7 @@ def setup_vault(vault: Vault, lxd: CLILXD, maas_container: str) -> None: |
1457 | vault.secrets_mount, |
1458 | ], |
1459 | ) |
1460 | - lxd.execute( |
1461 | - maas_container, |
1462 | + maas_container.execute( |
1463 | [ |
1464 | "maas", |
1465 | "config-vault", |
UNIT TESTS
-b lxd-instance-facade lp:~adam-collard/maas-ci/+git/system-tests into -b master lp:~maas-committers/maas-ci/+git/system-tests
STATUS: SUCCESS b2e91c94f943eac 045193e75c
COMMIT: ceb52ed309df1e1