Merge ~lloydwaltersj/maas-ci/+git/system-tests:image_mapping-parity-with-packerMAAS into ~maas-committers/maas-ci/+git/system-tests:master

Proposed by Jack Lloyd-Walters
Status: Needs review
Proposed branch: ~lloydwaltersj/maas-ci/+git/system-tests:image_mapping-parity-with-packerMAAS
Merge into: ~maas-committers/maas-ci/+git/system-tests:master
Diff against target: 813 lines (+346/-79)
13 files modified
image_mapping.yaml (+67/-7)
lxd_configs/packer.profile (+0/-1)
pyproject.toml (+1/-0)
systemtests/api.py (+7/-5)
systemtests/conftest.py (+4/-0)
systemtests/fixtures.py (+14/-0)
systemtests/image_builder/test_packer.py (+18/-2)
systemtests/image_config.py (+10/-1)
systemtests/packer.py (+73/-9)
systemtests/state.py (+48/-17)
systemtests/tests_per_machine/test_hardware_sync.py (+6/-0)
systemtests/tests_per_machine/test_machine.py (+3/-0)
utils/gen_config.py (+95/-37)
Reviewer Review Type Date Requested Status
MAAS Lander Approve
MAAS Committers Pending
Review via email: mp+462765@code.launchpad.net

Commit message

Add images: Alma8, Alma9
Add Series: Ubuntu:Noble
Add Arches: Ubuntu:arm64, Debian:arm64
Add Arch support in gen_config and image_builder
Skips tests if an incompatible machine and image arch are generated

Description of the change

Packer MAAS has become slightly out of sync with the system tests, let's fix that:

- Add all the images packer maas supports to the image mapping
- Add all the arches we have confirmed working to the image mapping
- Add the ability to build said arches
- Add image testing skipping if the machine and image arch do not match

To post a comment you must log in.
d5991c7... by Jack Lloyd-Walters

fix example in genconfig

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b image_mapping-parity-with-packerMAAS lp:~lloydwaltersj/maas-ci/+git/system-tests into -b master lp:~maas-committers/maas-ci/+git/system-tests

STATUS: SUCCESS
COMMIT: a7f9e364e0798b430698134844c58ed858c28e5f

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b image_mapping-parity-with-packerMAAS lp:~lloydwaltersj/maas-ci/+git/system-tests into -b master lp:~maas-committers/maas-ci/+git/system-tests

STATUS: SUCCESS
COMMIT: d5991c707c23949df63ed8006f30fca0a333ad7c

review: Approve
590856f... by Jack Lloyd-Walters

add alma and debian to pretty os name

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b image_mapping-parity-with-packerMAAS lp:~lloydwaltersj/maas-ci/+git/system-tests into -b master lp:~maas-committers/maas-ci/+git/system-tests

STATUS: SUCCESS
COMMIT: 590856f1110915e0f9b0b2a6657b43c0a0f945af

review: Approve
f63a5b5... by Jack Lloyd-Walters

better errors

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b image_mapping-parity-with-packerMAAS lp:~lloydwaltersj/maas-ci/+git/system-tests into -b master lp:~maas-committers/maas-ci/+git/system-tests

STATUS: SUCCESS
COMMIT: f63a5b5e7ff31e9b6116233085e75d2884a696e1

review: Approve
98d4c66... by Jack Lloyd-Walters

don't add arch to QEMU filename

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b image_mapping-parity-with-packerMAAS lp:~lloydwaltersj/maas-ci/+git/system-tests into -b master lp:~maas-committers/maas-ci/+git/system-tests

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/system-tests-tester/1031/console
COMMIT: 98d4c66dbff4aef5ad7811c1b5ad64c280fe12dd

review: Needs Fixing
8ccb147... by Jack Lloyd-Walters

add noble to image mapping

1b26db2... by Jack Lloyd-Walters

add qemu arm package

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b image_mapping-parity-with-packerMAAS lp:~lloydwaltersj/maas-ci/+git/system-tests into -b master lp:~maas-committers/maas-ci/+git/system-tests

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/system-tests-tester/1032/console
COMMIT: 1b26db2087c260dda383008ef827fb93e77f1558

review: Needs Fixing
c7b0b4f... by Jack Lloyd-Walters

add timeout option to packer

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b image_mapping-parity-with-packerMAAS lp:~lloydwaltersj/maas-ci/+git/system-tests into -b master lp:~maas-committers/maas-ci/+git/system-tests

STATUS: SUCCESS
COMMIT: c7b0b4f341c5ad6c444d1bc0e1667315fe20a902

review: Approve
91153a3... by Jack Lloyd-Walters

add more granular arch support

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b image_mapping-parity-with-packerMAAS lp:~lloydwaltersj/maas-ci/+git/system-tests into -b master lp:~maas-committers/maas-ci/+git/system-tests

STATUS: SUCCESS
COMMIT: 91153a35b87ddf614296238f10e7e939b5bcd446

review: Approve
26b8463... by Jack Lloyd-Walters

add arm64 to ubuntu-flat

035b101... by Jack Lloyd-Walters

add arch to build log file name

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b image_mapping-parity-with-packerMAAS lp:~lloydwaltersj/maas-ci/+git/system-tests into -b master lp:~maas-committers/maas-ci/+git/system-tests

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/system-tests-tester/1035/console
COMMIT: 035b10196e7e6b5c384f1adff13f07716f74649f

review: Needs Fixing
48bd337... by Jack Lloyd-Walters

linting

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b image_mapping-parity-with-packerMAAS lp:~lloydwaltersj/maas-ci/+git/system-tests into -b master lp:~maas-committers/maas-ci/+git/system-tests

STATUS: SUCCESS
COMMIT: 48bd337afbdf2647cd69eba87a9a798f82adbecb

review: Approve
Revision history for this message
Jack Lloyd-Walters (lloydwaltersj) wrote :

jenkins: !test

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b image_mapping-parity-with-packerMAAS lp:~lloydwaltersj/maas-ci/+git/system-tests into -b master lp:~maas-committers/maas-ci/+git/system-tests

STATUS: SUCCESS
COMMIT: 48bd337afbdf2647cd69eba87a9a798f82adbecb

review: Approve
15db22e... by Jack Lloyd-Walters

skip if architecture wrong

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b image_mapping-parity-with-packerMAAS lp:~lloydwaltersj/maas-ci/+git/system-tests into -b master lp:~maas-committers/maas-ci/+git/system-tests

STATUS: SUCCESS
COMMIT: 15db22ed6f7fd471369cc2a7c9c73bd947253128

review: Approve

Unmerged commits

15db22e... by Jack Lloyd-Walters

skip if architecture wrong

Succeeded
[SUCCEEDED] lint:0 (build)
11 of 1 result
48bd337... by Jack Lloyd-Walters

linting

Succeeded
[SUCCEEDED] lint:0 (build)
11 of 1 result
035b101... by Jack Lloyd-Walters

add arch to build log file name

Failed
[FAILED] lint:0 (build)
11 of 1 result
26b8463... by Jack Lloyd-Walters

add arm64 to ubuntu-flat

91153a3... by Jack Lloyd-Walters

add more granular arch support

Succeeded
[SUCCEEDED] lint:0 (build)
11 of 1 result
c7b0b4f... by Jack Lloyd-Walters

add timeout option to packer

Succeeded
[SUCCEEDED] lint:0 (build)
11 of 1 result
1b26db2... by Jack Lloyd-Walters

add qemu arm package

Failed
[FAILED] lint:0 (build)
11 of 1 result
8ccb147... by Jack Lloyd-Walters

add noble to image mapping

98d4c66... by Jack Lloyd-Walters

don't add arch to QEMU filename

Failed
[FAILED] lint:0 (build)
11 of 1 result
f63a5b5... by Jack Lloyd-Walters

better errors

Succeeded
[SUCCEEDED] lint:0 (build)
11 of 1 result

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/image_mapping.yaml b/image_mapping.yaml
2index 69a0eea..32b2af7 100644
3--- a/image_mapping.yaml
4+++ b/image_mapping.yaml
5@@ -15,14 +15,55 @@
6 # packer_target: $PACKER_TARGET
7 # source_iso: $ISO_NAME
8
9-# Also includes a mapping between main packer image and series
10-# name, for the images that accept that argument (ie: ubuntu)
11-
12+# Includes a mapping between main packer image and series: `images_with_series`
13 # some images wrap multiple series, which are buildable by passing
14 # `SERIES=$series` during make. This maps those images, along with
15-# the valid series that are passable
16+# the valid series that are passable. ie:
17+# images_with_series:
18+# $IMAGE:
19+# - $SERIES_1
20+# - $SERIES_2
21+
22+# Includes a mapping between packer image and architecture: `images_with_arches`
23+# some images are buildable for different architectures, which are
24+# accessed by passing `ARCH=$arch` during make. This maps the architectures
25+# buildable to the image, alongside any per-series support for an arch, ie:
26+# images_with_arches:
27+# $IMAGE:
28+# arches:
29+# - $ARCH_1
30+# - $ARCH_2
31+# supported_series:
32+# $ARCH_2:
33+# - $SERIES_1
34+# - $SERIES_2
35+# $IMAGE_WITH_CONF:
36+# arches:
37+# - arch_2
38+# if an arch is not found in supported_Series, it is assumed that all series
39+# for the image support all arches. (If the image does not define a series, this
40+# can obviously be ignored). If an image has a configuration option, we search
41+# for that first, defaulting to the base image (ie: ubuntu-flat -> ubuntu)
42
43 images:
44+ alma8:
45+ filename: alma8.tar.gz
46+ filetype: tgz
47+ architecture: amd64/generic
48+ osystem: custom
49+ oseries: "alma8"
50+ packer_template: alma8
51+ base_image: "rhel/8"
52+ ssh_username: cloud-user
53+ alma9:
54+ filename: alma9.tar.gz
55+ filetype: tgz
56+ architecture: amd64/generic
57+ osystem: custom
58+ oseries: "alma9"
59+ packer_template: alma9
60+ base_image: "rhel/9"
61+ ssh_username: cloud-user
62 centos6:
63 filename: centos6.tar.gz
64 filetype: tgz
65@@ -202,7 +243,12 @@ images:
66 oseries: "ubuntu-raw-lvm"
67 packer_template: ubuntu
68 packer_target: custom-ubuntu-lvm.dd.gz
69+
70 images_with_series:
71+ debian:
72+ - buster
73+ - bullseye
74+ - bookworm
75 ubuntu:
76 - xenial
77 - bionic
78@@ -210,7 +256,21 @@ images_with_series:
79 - jammy
80 - lunar
81 - mantic
82+ - noble
83+
84+images_with_arches:
85 debian:
86- - buster
87- - bullseye
88- - bookworm
89+ arches:
90+ - amd64
91+ - arm64
92+ ubuntu:
93+ arches:
94+ - amd64
95+ - arm64
96+ ubuntu-flat:
97+ arches:
98+ - amd64
99+ - arm64
100+ ubuntu-lvm:
101+ arches:
102+ - amd64
103diff --git a/lxd_configs/packer.profile b/lxd_configs/packer.profile
104index e2772ec..b3c5740 100644
105--- a/lxd_configs/packer.profile
106+++ b/lxd_configs/packer.profile
107@@ -5,4 +5,3 @@ devices:
108 path: /root/.cache/packer
109 source: /home/ubuntu/packer/cache
110 type: disk
111- shift: true
112diff --git a/pyproject.toml b/pyproject.toml
113index f8d44e5..5eb0f4a 100644
114--- a/pyproject.toml
115+++ b/pyproject.toml
116@@ -8,6 +8,7 @@ markers = [
117 "skip_if_installed_from_deb_package", # Skips tests if MAAS is installed from package.
118 "skip_if_packer_unconfigured", # Skips tests if Packer-MAAS is not configured.
119 "skip_if_os_not_supported", # Skips tests if the OS chosen is not compatible with the machine architecture
120+ "skip_if_arch_incompatible", # Skips test if the machine and image arches do not match
121 "skip_if_terraform_unconfigured" # Skips tests if Terraform is not configured.
122 ]
123 log_level = "INFO"
124diff --git a/systemtests/api.py b/systemtests/api.py
125index 894f93b..1c31099 100644
126--- a/systemtests/api.py
127+++ b/systemtests/api.py
128@@ -352,12 +352,14 @@ class AuthenticatedAPIClient:
129
130 def pretty_os_name(self, osystem: str) -> str:
131 oses = {
132+ "alma": "AlmaLinux",
133 "centos": "CentOS",
134+ "debian": "Debian",
135+ "esxi": "ESXi",
136 "ol": "OracleLinux",
137 "rhel": "RHEL",
138 "rocky": "RockyLinux",
139 "sles": "SLES",
140- "esxi": "ESXi",
141 "ubuntu": "Ubuntu",
142 "windows": "Windows",
143 }
144@@ -379,8 +381,8 @@ class AuthenticatedAPIClient:
145 f"Available images: {', '.join(resources)}"
146 )
147
148- def wait_for_images(self) -> None:
149- for retry_info in retries(timeout=20 * 60, delay=10):
150+ def wait_for_images(self, timeout: int = 30 * 60) -> None:
151+ for retry_info in retries(timeout=timeout, delay=10):
152 if not self.is_importing_boot_resources():
153 break
154 else:
155@@ -389,9 +391,9 @@ class AuthenticatedAPIClient:
156 f" over {retry_info.elapsed} seconds."
157 )
158
159- def import_and_wait_for_images(self) -> None:
160+ def import_and_wait_for_images(self, timeout: int = 30 * 60) -> None:
161 self.import_boot_resources()
162- self.wait_for_images()
163+ self.wait_for_images(timeout=timeout)
164
165 def list_machines(self, **kwargs: Any) -> list[Machine]:
166 """
167diff --git a/systemtests/conftest.py b/systemtests/conftest.py
168index a7e9111..4453271 100644
169--- a/systemtests/conftest.py
170+++ b/systemtests/conftest.py
171@@ -52,6 +52,7 @@ from .fixtures import (
172 pool,
173 skip_if_ansible_collection_unconfigured,
174 skip_if_ansible_playbooks_unconfigured,
175+ skip_if_arch_incompatible,
176 skip_if_dns_unconfigured,
177 skip_if_installed_from_deb_package,
178 skip_if_installed_from_snap,
179@@ -70,6 +71,7 @@ from .lxd import Instance, get_lxd
180 from .machine_config import MachineConfig
181 from .models import HardwareSyncMachine, MachineUnderTest
182 from .o11y import add_o11y_header
183+from .packer import add_packer_header
184 from .state import (
185 authenticated_admin,
186 configured_maas,
187@@ -114,6 +116,7 @@ __all__ = [
188 "skip_if_installed_from_deb_package",
189 "skip_if_installed_from_snap",
190 "skip_if_os_not_supported",
191+ "skip_if_arch_incompatible",
192 "ssh_key",
193 "tag_all",
194 "terraform_main",
195@@ -204,6 +207,7 @@ def pytest_report_header(config: pytest.Config) -> list[str]:
196 add_vault_header(headers, systemtests_config)
197 add_o11y_header(headers, systemtests_config)
198 add_ansible_header(headers, systemtests_config)
199+ add_packer_header(headers, systemtests_config)
200 return headers + ["END_SYSTEMTESTS_META"]
201
202
203diff --git a/systemtests/fixtures.py b/systemtests/fixtures.py
204index 20f856f..473046c 100644
205--- a/systemtests/fixtures.py
206+++ b/systemtests/fixtures.py
207@@ -17,6 +17,7 @@ from .ansible_collection import AnsibleCollectionMain
208 from .api import AuthenticatedAPIClient, UnauthenticatedMAASAPIClient
209 from .config import ADMIN_EMAIL, ADMIN_PASSWORD, ADMIN_USER
210 from .dnstester import DNSTester
211+from .image_config import TestableImage
212 from .lxd import Instance, get_lxd
213 from .machine_config import MachineConfig
214 from .o11y import is_o11y_enabled, setup_o11y
215@@ -705,6 +706,19 @@ def skip_if_os_not_supported(request: Any) -> None:
216
217
218 @pytest.fixture(autouse=True)
219+def skip_if_arch_incompatible(request: Any) -> None:
220+ if marker := request.node.get_closest_marker("skip_if_arch_incompatible"):
221+ # Avoid instantiating machine or image configurations unless marker applied
222+ machine_config: MachineConfig = request.getfixturevalue("machine_config")
223+ image_to_test: TestableImage = request.getfixturevalue("image_to_test")
224+ reason = marker.args[0]
225+ if image_to_test and (
226+ image_to_test.architecture != machine_config.architecture
227+ ):
228+ pytest.skip(reason)
229+
230+
231+@pytest.fixture(autouse=True)
232 def skip_if_packer_unconfigured(request: Any, config: dict[str, Any]) -> None:
233 """Skip tests that require Packer configuration."""
234 marker = request.node.get_closest_marker("skip_if_packer_unconfigured")
235diff --git a/systemtests/image_builder/test_packer.py b/systemtests/image_builder/test_packer.py
236index 83f0302..c015f57 100644
237--- a/systemtests/image_builder/test_packer.py
238+++ b/systemtests/image_builder/test_packer.py
239@@ -32,20 +32,36 @@ class TestPackerMAASConfig:
240 deps = packer_main.get_dependencies(image_to_build.packer_template)
241 testlog.info(f"Image Dependencies: {deps}")
242
243- image, preseed = packer_main.build_image(
244+ image, alt_image, preseed = packer_main.build_image(
245 image_to_build.packer_template,
246 image_to_build.packer_target,
247 image_to_build.packer_filename,
248+ image_to_build.alternative_packer_filename,
249 image_to_build.build_series,
250+ image_to_build.build_architecture,
251 image_to_build.preseed,
252 image_to_build.source_iso,
253 )
254 assert image is not None
255 img_file = packer_main._instance.files[image]
256- assert img_file.exists(), f"failed to produce the expected image ({img_file})"
257+ if not img_file.exists() and alt_image and alt_image != image:
258+ packer_main.logger.error(
259+ f"Trying alternative filename {alt_image}, {image} doesn't exist."
260+ )
261+ img_file = packer_main._instance.files[alt_image]
262+ assert (
263+ img_file.exists()
264+ ), f"Failed to produce the expected image ({img_file.path})"
265
266 if image_to_build.filename:
267 packer_main.upload_image(img_file, image_to_build.filename)
268+ packer_main.logger.info(
269+ f"{image_to_build.name} uploaded as {image_to_build.filename}"
270+ )
271+
272 if preseed_filename := image_to_build.preseed_filename:
273 preseed_file = packer_main._instance.files[preseed]
274 packer_main.upload_preseed(preseed_file, preseed_filename)
275+ packer_main.logger.info(
276+ f"{image_to_build.name} preseed uploaded as {preseed_filename}"
277+ )
278diff --git a/systemtests/image_config.py b/systemtests/image_config.py
279index 57bbe0f..556e728 100644
280--- a/systemtests/image_config.py
281+++ b/systemtests/image_config.py
282@@ -31,11 +31,12 @@ class TestableImage:
283 packer_template: str | None = None
284 packer_target: str | None = None
285 build_series: str | None = None
286+ build_architecture: str | None = None
287 preseed: str | None = None
288 source_iso: str | None = None
289
290 def __str__(self) -> str:
291- return f"{self.osystem}/{self.oseries}"
292+ return self.filename.split(".")[0]
293
294 @classmethod
295 def from_config(
296@@ -59,6 +60,14 @@ class TestableImage:
297 return self.packer_target or f"{self.packer_template}.{ext}"
298
299 @property
300+ def alternative_packer_filename(self) -> str:
301+ """If the packer name doesn't match, try this."""
302+ ext = EXTENSION_MAP[self.filetype]
303+ if self.packer_template is None:
304+ return f"{self.name}.{ext}"
305+ return self.packer_target or f"{self.packer_template}.{ext}"
306+
307+ @property
308 def preseed_filename(self) -> str:
309 if preseed := self.preseed:
310 return "_".join(
311diff --git a/systemtests/packer.py b/systemtests/packer.py
312index e50314a..e33e962 100644
313--- a/systemtests/packer.py
314+++ b/systemtests/packer.py
315@@ -1,5 +1,6 @@
316 from __future__ import annotations
317
318+import re
319 from datetime import timedelta
320 from pathlib import Path
321 from typing import TYPE_CHECKING, Any
322@@ -11,14 +12,26 @@ from systemtests.lxd import _FileWrapper
323 if TYPE_CHECKING:
324 from systemtests.lxd import CLILXD, Instance
325
326+TIMEOUT_REGEX = re.compile(r"(?:(?P<d>\d+) days, )?(?P<h>\d+)\:(?P<m>\d+)\:\d+")
327+
328
329 class UnknowStorageBackendError(Exception):
330 """We don't know how to handle this backend"""
331
332
333+def add_packer_header(headers: list[str], config: dict[str, Any]) -> None:
334+ if packer_images := config.get("image-tests", {}):
335+ headers.append(f"packer images: {', '.join(packer_images.keys())}")
336+ if packer_config := config.get("packer-maas", {}):
337+ git_repo = packer_config.get(
338+ "git-repo", "https://github.com/canonical/packer-maas"
339+ )
340+ git_branch = packer_config.get("git-branch", "main")
341+ headers.append(f"packer: {git_repo}:{git_branch}")
342+
343+
344 class PackerMain(GitBuild):
345- use_timeout = True
346- timeout = timedelta(minutes=15)
347+ timeout = timedelta(hours=2)
348
349 def __init__(
350 self,
351@@ -61,6 +74,7 @@ class PackerMain(GitBuild):
352 self.apt_install(
353 [
354 "cloud-image-utils",
355+ "efibootmgr",
356 "fuse2fs",
357 "fusefat",
358 "libnbd-bin",
359@@ -68,7 +82,13 @@ class PackerMain(GitBuild):
360 "nbdkit",
361 "ovmf",
362 "python3-pip",
363+ "qemu-utils",
364+ # boot
365+ "qemu-efi",
366+ "qemu-efi-aarch64",
367+ # architectures
368 "qemu-system-x86",
369+ "qemu-system-aarch64",
370 ]
371 )
372 self.logger.info("Installing Packer")
373@@ -110,13 +130,29 @@ class PackerMain(GitBuild):
374 packer_template: str,
375 packer_target: str | None,
376 img_filename: str,
377+ img_alt_filename: str | None,
378 build_series: str | None,
379+ build_architecture: str | None,
380 preseed_file: str | None,
381 source_iso: str | None,
382- ) -> tuple[str | None, str]:
383+ ) -> tuple[str | None, str | None, str]:
384 env = self._env.copy()
385 env["SUDO"] = "sudo -E"
386- log_file = f"build-{packer_template}-{packer_target or 'all'}.log"
387+ log_file = (
388+ "-".join(
389+ filter(
390+ None,
391+ [
392+ "build",
393+ packer_template,
394+ packer_target or "all",
395+ build_architecture,
396+ ],
397+ )
398+ )
399+ + ".log"
400+ )
401+ env["FOREGROUND"] = "1"
402 env["PACKER_LOG"] = "on"
403 env["PACKER_LOG_PATH"] = f"{self.clone_path}/{log_file}"
404 if source_iso:
405@@ -129,9 +165,13 @@ class PackerMain(GitBuild):
406 "-C",
407 f"{self.clone_path}/{packer_template}",
408 f"{packer_target or 'all'}",
409+ "PACKER_LOG=1",
410 ]
411 if build_series:
412 cmd += [f"SERIES={build_series}"]
413+ if build_architecture:
414+ cmd += [f"ARCH={build_architecture}", f"TIMEOUT={self.timeout_units}"]
415+
416 try:
417 runtime = self.timed(
418 self._instance.execute,
419@@ -149,11 +189,20 @@ class PackerMain(GitBuild):
420
421 # if the image has a build series, the default file may not match
422 image_file_path = f"{self.clone_path}/{packer_template}/{img_filename}"
423- return (
424- image_file_path.replace(f"-{build_series}", "")
425- if build_series
426- else image_file_path
427- ), preseed
428+ alternate_file_path = f"{self.clone_path}/{packer_template}/{img_alt_filename}"
429+
430+ # the result does not contain an arch name, but the filename given might
431+ if arch := build_architecture:
432+ image_file_path = image_file_path.replace(f":{arch}", "")
433+ alternate_file_path = alternate_file_path.replace(f":{arch}", "")
434+
435+ if build_series:
436+ return (
437+ image_file_path.replace(f"-{build_series}", ""),
438+ alternate_file_path.replace(f"-{build_series}", ""),
439+ preseed,
440+ )
441+ return image_file_path, alternate_file_path, preseed
442
443 def get_dependencies(
444 self,
445@@ -173,3 +222,18 @@ class PackerMain(GitBuild):
446
447 def __repr__(self) -> str:
448 return f"<PackerMain in {self._instance}>"
449+
450+ @property
451+ def timeout_units(self) -> str:
452+ if search := TIMEOUT_REGEX.search(str(self.timeout)):
453+ return "".join(
454+ filter(
455+ None,
456+ [
457+ f"{value}{unit}"
458+ for unit, value in search.groupdict().items()
459+ if value
460+ ],
461+ )
462+ )
463+ raise ValueError(f"{self.timeout} didn't match the expected form?")
464diff --git a/systemtests/state.py b/systemtests/state.py
465index 92aba9f..313da9d 100644
466--- a/systemtests/state.py
467+++ b/systemtests/state.py
468@@ -65,12 +65,24 @@ def import_images_and_wait_until_synced(
469 ) -> None:
470 architectures = set()
471 osystems: set[tuple[str, str]] = set()
472- for power_config in config.get("machines", {}).get("hardware", {}).values():
473+
474+ # fetch the configs
475+ per_machine_config: dict[str, Any] = config.get("machines", {}).get("hardware", {})
476+ all_vm_config: dict[str, Any] = config.get("machines", {})
477+ per_vm_config: dict[str, Any] = all_vm_config.get("vms", {}).get("instances", {})
478+
479+ # determine osystems and arches per machine
480+ for power_config in (per_machine_config | per_vm_config | all_vm_config).values():
481 architectures.add(power_config.get("architecture", "amd64"))
482 if set(("osystem", "oseries")).intersection(power_config.keys()):
483- osystem = power_config.get("osystem", "ubuntu")
484- oseries = power_config.get("oseries", "focal")
485- osystems.add((osystem, oseries))
486+ osystems.add(
487+ (
488+ power_config.get("osystem", "ubuntu"),
489+ power_config.get("oseries", "focal"),
490+ )
491+ )
492+
493+ # determine all osystems per terrafrom and ansible
494 osystems.update(
495 set(
496 ("ubuntu", series)
497@@ -82,6 +94,10 @@ def import_images_and_wait_until_synced(
498 )
499 )
500 )
501+ LOG.info(
502+ f"{len(osystems)} OS Images selected for import: "
503+ + ", ".join("-".join(osys) for osys in osystems)
504+ )
505
506 started_importing_regex = "^Started importing of boot images from"
507 # Sometimes the region has already started importing images, we
508@@ -138,31 +154,37 @@ def import_images_and_wait_until_synced(
509 for name, cfg in config.get("image-tests", {}).items()
510 ]
511 for img in custom_images:
512+ dst = authenticated_admin.api_client.maas_container.files[
513+ f"/home/ubuntu/{img.filename}"
514+ ]
515+ preseed_dst = authenticated_admin.api_client.maas_container.files[
516+ f"/home/ubuntu/{img.filename}"
517+ ]
518+
519 scheme, filepath = get_filestore_filepath(file_store_cfg, img.filename)
520 if scheme == "file":
521- dst = authenticated_admin.api_client.maas_container.files[
522- f"/home/ubuntu/{img.filename}"
523- ]
524 dst.push(filepath, create_dirs=True, uid=1000, gid=1000)
525+ LOG.info(f"Uploaded {img}")
526 else:
527 LOG.error(f"Unsupported file-store scheme '{scheme}'")
528
529- # upload the preseed if needed
530 if not img.preseed:
531 continue
532+
533 preseed_scheme, preseed_filepath = get_filestore_filepath(
534 file_store_cfg, img.preseed_filename
535 )
536 if preseed_scheme == "file":
537- dst = authenticated_admin.api_client.maas_container.files[
538- f"/var/snap/maas/current/preseeds/{img.preseed_filename}"
539- ]
540- dst.push(preseed_filepath, create_dirs=True, uid=1000, gid=1000)
541+ preseed_dst.push(preseed_filepath, create_dirs=True, uid=1000, gid=1000)
542+ LOG.info(f"Uploaded {img} preseed")
543 else:
544 LOG.error(f"Unsupported file-store scheme '{preseed_scheme}'")
545
546 region_start_point = time.time()
547- authenticated_admin.import_and_wait_for_images()
548+ # allow up to 40 minutes per image
549+ authenticated_admin.import_and_wait_for_images(
550+ timeout=40 * 60 * max(1, len(custom_images))
551+ )
552 authenticated_admin.log_available_boot_resources()
553
554 region_time_taken = time.time() - region_start_point
555@@ -184,7 +206,7 @@ def import_images_and_wait_until_synced(
556 LOG.info(f"Took {windows_time_taken:0.1f}s to upload Windows")
557 # testing of custom (packer) images
558 for img in custom_images:
559- architectures.add(img.architecture)
560+ architectures.add(img.architecture.split("/")[0])
561 osystems.add((img.osystem, img.oseries))
562 custom_image_start_point = time.time()
563 img_params = {
564@@ -196,6 +218,7 @@ def import_images_and_wait_until_synced(
565 }
566 if img.base_image is not None:
567 img_params["base_image"] = img.base_image
568+
569 authenticated_admin.create_boot_resource(**img_params)
570 custom_images_time_taken = time.time() - custom_image_start_point
571 LOG.info(f"Took {custom_images_time_taken:0.1f}s to upload {img.name}")
572@@ -306,14 +329,22 @@ def ready_remote_maas(
573 for image in boot_images["images"]
574 if image["name"].startswith("bootloader/")
575 }
576+
577 image_arches = {
578 image["architecture"]
579 for image in boot_images["images"]
580 if not image["name"].startswith("bootloader/")
581 }
582- assert needed_architectures <= bootloaders_arches
583- assert needed_architectures <= image_arches
584- assert boot_images["status"] == "synced"
585+ # Make failure explicit for failure scanning
586+ assert needed_architectures <= bootloaders_arches, (
587+ "Image architectures missing! "
588+ f"{needed_architectures.difference(bootloaders_arches)} "
589+ f"not in {bootloaders_arches}"
590+ )
591+ assert needed_architectures <= image_arches, (
592+ "Image architectures missing! "
593+ f"{needed_architectures.difference(image_arches)} not in {image_arches}"
594+ )
595
596
597 # FIXME: scope should be function, and use: @pytest_steps.cross_steps_fixture
598diff --git a/systemtests/tests_per_machine/test_hardware_sync.py b/systemtests/tests_per_machine/test_hardware_sync.py
599index d2544cc..f5f853c 100644
600--- a/systemtests/tests_per_machine/test_hardware_sync.py
601+++ b/systemtests/tests_per_machine/test_hardware_sync.py
602@@ -6,6 +6,7 @@ from itertools import chain, repeat
603 from subprocess import CalledProcessError, CompletedProcess
604 from typing import TYPE_CHECKING, Callable, Iterator
605
606+import pytest
607 from pytest_steps import test_steps
608 from pytest_steps.steps_generator import optional_step
609 from retry.api import retry, retry_call
610@@ -24,6 +25,7 @@ if TYPE_CHECKING:
611 from paramiko import PKey
612
613 from ..api import AuthenticatedAPIClient, Machine
614+ from ..image_config import TestableImage
615 from ..lxd import Instance
616
617
618@@ -101,12 +103,16 @@ def powered_off_vm(instance: Instance) -> Iterator[None]:
619 "remove_device",
620 "cleanup",
621 )
622+@pytest.mark.skip_if_arch_incompatible(
623+ "Machine architecture incompatible with image architecture"
624+)
625 def test_hardware_sync(
626 maas_api_client: AuthenticatedAPIClient,
627 hardware_sync_machine: HardwareSyncMachine,
628 ready_remote_maas: None,
629 ssh_key: PKey,
630 testlog: Logger,
631+ image_to_test: TestableImage | None,
632 ) -> Iterator[None]:
633 hardware_sync_machine.instance.logger = testlog
634 maas_api_client.set_config("hardware_sync_interval", "5s")
635diff --git a/systemtests/tests_per_machine/test_machine.py b/systemtests/tests_per_machine/test_machine.py
636index 82a70b6..e272113 100644
637--- a/systemtests/tests_per_machine/test_machine.py
638+++ b/systemtests/tests_per_machine/test_machine.py
639@@ -30,6 +30,9 @@ if TYPE_CHECKING:
640
641
642 @pytest.mark.skip_if_os_not_supported("Machine hardware is not supported by OS")
643+@pytest.mark.skip_if_arch_incompatible(
644+ "Machine architecture incompatible with image architecture"
645+)
646 @test_steps("enlist", "metadata", "commission", "deploy", "test_image", "rescue")
647 def test_full_circle(
648 maas_api_client: AuthenticatedAPIClient,
649diff --git a/utils/gen_config.py b/utils/gen_config.py
650index 9679636..56bf890 100755
651--- a/utils/gen_config.py
652+++ b/utils/gen_config.py
653@@ -4,6 +4,7 @@
654 from __future__ import annotations
655
656 import argparse
657+import re
658 import sys
659 from typing import Any, Optional
660
661@@ -11,6 +12,9 @@ from ruamel.yaml import YAML
662
663 yaml = YAML()
664
665+IMAGE_ARCH_REGEX = re.compile(r"^(?P<name_tuple>[^\:]+)(?:\:(?P<arch>\w+))?$")
666+IMAGE_SERIES_REGEX = re.compile(r"(?:^(?P<name_part>.*)\-)*(?P<series>[^\-]+)$")
667+
668
669 class WideHelpFormatter(argparse.HelpFormatter):
670 def __init__(
671@@ -151,7 +155,14 @@ def main(argv: list[str]) -> int:
672 "--image-name",
673 action="append",
674 metavar="IMAGE_NAME",
675- help="Run tests for this image; can be repeated",
676+ help="""
677+ Run tests for this image; can be repeated.
678+ If supported by the image, image names can contain series and arch information:
679+ - base image: centos7
680+ - image with series: debian-buster
681+ - image with arch: ubuntu:arm64
682+ - series and arch: ubuntu-noble:arm64
683+ """,
684 )
685 parser.add_argument(
686 "--image-filestore",
687@@ -338,44 +349,91 @@ def main(argv: list[str]) -> int:
688 config["image-tests"] = config.get("image-tests", {})
689 with args.image_mapping as fh:
690 image_cfg: dict[str, Any] = yaml.load(fh)
691- # fetch details from the image mapping cfg file
692- for image in args.image_name:
693- images = image_cfg.get("images", {})
694- series_images = image_cfg.get("images_with_series", {})
695-
696- # is this an image with a series argument?
697- if image_search := set([image, image.split("-")[0]]).intersection(
698- series_images.keys()
699- ):
700- image_name = list(image_search)[0]
701- if "-" in image:
702- packer_name, series = image.rsplit("-", 1)
703- if series in series_images.get(image_name):
704- config["image-tests"][image] = image_cfg["images"][packer_name]
705- config["image-tests"][image]["build_series"] = series
706- filename = config["image-tests"][image]["filename"].split(
707- ".", 1
708+
709+ images = image_cfg.get("images", {})
710+ series_images = image_cfg.get("images_with_series", {})
711+ arches_images = image_cfg.get("images_with_arches", {})
712+ mapping_filename = args.image_mapping.name
713+
714+ for image_name in args.image_name:
715+
716+ image, series, arch = None, None, None
717+
718+ if arch_search := IMAGE_ARCH_REGEX.search(image_name):
719+
720+ image, arch = arch_search.groups()
721+
722+ if image not in images and (
723+ series_search := IMAGE_SERIES_REGEX.search(image)
724+ ):
725+ image, series = series_search.groups()
726+
727+ base_image = image.split("-")[0]
728+
729+ assert (
730+ image in images
731+ ), f"{image.capitalize()} isn't defined in {mapping_filename}!"
732+
733+ this_image_cfg = dict(images[image])
734+
735+ if series:
736+ assert (
737+ base_image in series_images
738+ ), f"{base_image.capitalize()} doesn't define any series!"
739+ assert (
740+ series in series_images[base_image]
741+ ), f"{base_image.capitalize()} doesn't have a series {series}!"
742+
743+ this_image_cfg["build_series"] = series
744+
745+ name, extension = this_image_cfg["filename"].split(".", 1)
746+ this_image_cfg["filename"] = f"{name}-{series}.{extension}"
747+
748+ this_image_cfg["oseries"] = (
749+ series
750+ if this_image_cfg["osystem"] == "custom"
751+ else f'{this_image_cfg["oseries"]}-{series}'
752+ )
753+
754+ if arch:
755+ image_key = image if image in arches_images else base_image
756+ assert (
757+ image_key in arches_images
758+ ), f"{image_key.capitalize()} doesn't define any architectures!"
759+ assert (
760+ arch in arches_images[image_key]["arches"]
761+ ), f"{image_key.capitalize()} doesn't have an architecture {arch}!"
762+ if (
763+ series
764+ and (
765+ supported_series := arches_images[image_key].get(
766+ "supported_series"
767+ )
768+ )
769+ and (arch_series_support := supported_series.get(arch))
770+ ):
771+ assert series in arch_series_support, (
772+ f"{image_key.capitalize()} doesn't support the "
773+ f"architecture {arch} on {series}!"
774 )
775- config["image-tests"][image][
776- "filename"
777- ] = f"{filename[0]}-{series}.{filename[1]}"
778- # append the series if the osystem is custom, else replace
779- if config["image-tests"][image]["osystem"] == "custom":
780- config["image-tests"][image]["oseries"] += f"-{series}"
781- else:
782- config["image-tests"][image]["oseries"] = series
783+
784+ default_arch, subarch = this_image_cfg["architecture"].split("/")
785+
786+ # only change stuff if we're not building the default arch
787+ if arch == default_arch:
788+ image_name = image_name.split(":")[0]
789+
790 else:
791- print(f"'{series}' is not a valid series for '{packer_name}'!")
792- else:
793- print(
794- f"'{image}' is invalid, {image_name}"
795- " images require a series argument!"
796- )
797- # This is a non-series image
798- elif image in images:
799- config["image-tests"][image] = image_cfg["images"][image]
800- else:
801- print(f"Image '{image}' does not have a known mapping!")
802+ this_image_cfg["architecture"] = f"{arch}/{subarch}"
803+ this_image_cfg["build_architecture"] = arch
804+
805+ name, extension = this_image_cfg["filename"].split(".", 1)
806+ this_image_cfg["filename"] = f"{name}:{arch}.{extension}"
807+
808+ if preseed := this_image_cfg.get("preseed"):
809+ this_image_cfg["preseed"] = preseed.replace(default_arch, arch)
810+
811+ config["image-tests"][image_name] = this_image_cfg
812 else:
813 config.pop("image-tests", None)
814

Subscribers

People subscribed via source and target branches