Merge ~adam-collard/maas-ci/+git/system-tests:lxd-instance-facade into ~maas-committers/maas-ci/+git/system-tests: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)
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

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

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
COMMIT: ceb52ed309df1e1b2e91c94f943eac045193e75c

review: Approve
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
1diff --git a/systemtests/api.py b/systemtests/api.py
2index 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
73diff --git a/systemtests/collect_sos_report/test_collect.py b/systemtests/collect_sos_report/test_collect.py
74index 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("."))
117diff --git a/systemtests/conftest.py b/systemtests/conftest.py
118index 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,
155diff --git a/systemtests/device_config.py b/systemtests/device_config.py
156index 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
172diff --git a/systemtests/env_builder/test_basic.py b/systemtests/env_builder/test_basic.py
173index 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"]:
258diff --git a/systemtests/fixtures.py b/systemtests/fixtures.py
259index 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)
742diff --git a/systemtests/lxd.py b/systemtests/lxd.py
743index 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)
974diff --git a/systemtests/o11y.py b/systemtests/o11y.py
975index 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])
1022diff --git a/systemtests/region.py b/systemtests/region.py
1023index 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:
1075diff --git a/systemtests/state.py b/systemtests/state.py
1076index 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",
1112diff --git a/systemtests/tests_per_machine/test_hardware_sync.py b/systemtests/tests_per_machine/test_hardware_sync.py
1113index 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
1241diff --git a/systemtests/tls.py b/systemtests/tls.py
1242index 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/"
1288diff --git a/systemtests/utils.py b/systemtests/utils.py
1289index 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 )
1343diff --git a/systemtests/vault.py b/systemtests/vault.py
1344index 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",

Subscribers

People subscribed via source and target branches

to all changes: