← Back to team overview

sts-sponsors team mailing list archive

[Merge] ~lloydwaltersj/maas-ci/+git/system-tests:system-test-image-test into ~maas-committers/maas-ci/+git/system-tests:master

 

Jack Lloyd-Walters has proposed merging ~lloydwaltersj/maas-ci/+git/system-tests:system-test-image-test into ~maas-committers/maas-ci/+git/system-tests:master.

Commit message:
allow tests_per_machine to use custom images

Requested reviews:
  MAAS Lander (maas-lander)
  MAAS Committers (maas-committers)

For more details, see:
https://code.launchpad.net/~lloydwaltersj/maas-ci/+git/system-tests/+merge/443284
-- 
Your team MAAS Committers is requested to review the proposed merge of ~lloydwaltersj/maas-ci/+git/system-tests:system-test-image-test into ~maas-committers/maas-ci/+git/system-tests:master.
diff --git a/image_mapping.yaml.sample b/image_mapping.yaml.sample
new file mode 100644
index 0000000..33b8e25
--- /dev/null
+++ b/image_mapping.yaml.sample
@@ -0,0 +1,13 @@
+---
+# Mapping between the names of built custom images, and the
+# details required to download/deploy them in systemtests
+
+# An example of a mapping is:
+# images:
+#    $IMAGE_NAME:
+#        url: $IMAGE_URL
+#        filetype: $IMAGE_FILETYPE
+#        architecture: $IMAGE_ARCH
+#        osystem: $IMAGE_OSYSTEM
+
+images:
diff --git a/systemtests/conftest.py b/systemtests/conftest.py
index 0db6532..78c27c9 100644
--- a/systemtests/conftest.py
+++ b/systemtests/conftest.py
@@ -54,6 +54,7 @@ from .fixtures import (
     vault,
     zone,
 )
+from .image_config import TestableImage
 from .lxd import Instance, get_lxd
 from .machine_config import MachineConfig
 from .o11y import add_o11y_header
@@ -287,6 +288,19 @@ def hardware_sync_machine(
 
 
 @pytest.fixture(scope="module")
+def images_to_test(request: Any) -> Iterator[TestableImage]:
+    images_to_test = request.param
+    yield images_to_test
+
+
+def generate_images(config: dict[str, Any]) -> list[TestableImage]:
+    return [
+        TestableImage.from_config(name=name, config=cfg)
+        for name, cfg in config.get("image-tests", {}).items()
+    ]
+
+
+@pytest.fixture(scope="module")
 def instance_config(
     authenticated_admin: AuthenticatedAPIClient, request: Any
 ) -> Iterator[MachineConfig]:
@@ -329,3 +343,9 @@ def pytest_generate_tests(metafunc: Metafunc) -> None:
             if len(instance.devices_config) > 0
         ]
         metafunc.parametrize("instance_config", instance_config, ids=str, indirect=True)
+
+    # if we're testing im
+    if "images_to_test" in metafunc.fixturenames:
+        images_to_test = [image for image in generate_images(cfg) if image.url]
+        LOG.info(f"Testing images: {', '.join(str(image) for image in images_to_test)}")
+        metafunc.parametrize("images_to_test", images_to_test, ids=str, indirect=True)
diff --git a/systemtests/image_config.py b/systemtests/image_config.py
new file mode 100644
index 0000000..15f5178
--- /dev/null
+++ b/systemtests/image_config.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Dict, Union, cast
+
+
+@dataclass(frozen=True)
+class TestableImage:
+    name: str
+    url: str
+    filetype: str = "targz"
+    architecture: str = "amd64"
+    osystem: str = "ubuntu"
+
+    def __str__(self) -> str:
+        return self.osystem
+
+    @classmethod
+    def from_config(
+        cls,
+        name: str,
+        config: dict[str, Union[str, dict[str, str]]],
+    ) -> TestableImage:
+        details = cast(Dict[str, str], config.copy())
+        return cls(
+            name=name,
+            **details,
+        )
diff --git a/systemtests/state.py b/systemtests/state.py
index 38494d0..7bb6faf 100644
--- a/systemtests/state.py
+++ b/systemtests/state.py
@@ -2,9 +2,11 @@ from __future__ import annotations
 
 import json
 import time
+from contextlib import closing
 from logging import getLogger
 from pathlib import Path
 from typing import TYPE_CHECKING, Any, Iterator, Set
+from urllib.request import urlopen
 
 import pytest
 from retry import retry
@@ -79,6 +81,19 @@ def import_images_and_wait_until_synced(
         authenticated_admin.api_client.maas_container.files[windows_path].push(
             Path(config["windows_image_file_path"]), uid=1000, gid=1000
         )
+    image_cfg = config.get("image-tests")
+    if image_cfg:
+        for image, cfg in image_cfg.items():
+            cfg["ftype"], ext = (
+                ("tgz", ".tar.gz") if "tar" in cfg["filetype"] else ("ddgz", ".dd.gz")
+            )
+            cfg["fpath"] = f"/home/ubuntu/{image}{ext}"
+            image_file = authenticated_admin.api_client.maas_container.files[
+                cfg["fpath"]
+            ]
+            LOG.debug(f"Downloading {image}{ext}")
+            with closing(urlopen(cfg["url"])) as response:
+                image_file.write(response.read().decode("utf-8"), uid=1000, gid=1000)
 
     region_start_point = time.time()
     while authenticated_admin.is_importing_boot_resources():
@@ -102,6 +117,23 @@ def import_images_and_wait_until_synced(
         )
         windows_time_taken = time.time() - windows_start_point
         LOG.info(f"Took {windows_time_taken:0.1f}s to upload Windows")
+    if image_cfg:
+        custom_image_start_point = time.time()
+        for image, cfg in image_cfg.items():
+            image_type = "Raw" if cfg["ftype"] == "ddgz" else "TGZ"
+            authenticated_admin.create_boot_resource(
+                name=f"custom/{image}-{cfg['ftype']}",
+                title=f"{image.capitalize()} Custom {image_type}",
+                architecture=cfg["architecture"],
+                filetype=cfg["ftype"],
+                image_file_path=cfg["fpath"],
+            )
+        custom_images_time_taken = time.time() - custom_image_start_point
+        LOG.info(
+            f"Took {custom_images_time_taken:0.1f}s to upload {len(image_cfg.keys())} "
+            + f"custom image{'s' if len(image_cfg.keys())>1 else ''}"
+        )
+
     rack_start_point = time.time()
     for rack_controller in get_rack_controllers(authenticated_admin):
         boot_images = authenticated_admin.list_boot_images(rack_controller)
diff --git a/systemtests/tests_per_machine/test_machine.py b/systemtests/tests_per_machine/test_machine.py
index 9241fed..4631504 100644
--- a/systemtests/tests_per_machine/test_machine.py
+++ b/systemtests/tests_per_machine/test_machine.py
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
     from paramiko import PKey
 
     from ..api import AuthenticatedAPIClient
+    from ..image_config import TestableImage
     from ..machine_config import MachineConfig
 
 
@@ -31,11 +32,13 @@ def test_full_circle(
     machine_config: MachineConfig,
     ready_remote_maas: None,
     maas_without_machine: None,
+    import_images_and_wait_until_synced: None,
     ssh_key: PKey,
     testlog: Logger,
     zone: str,
     pool: str,
     tag_all: str,
+    images_to_test: TestableImage,
 ) -> Iterator[None]:
     maas_ip_ranges = maas_api_client.list_ip_ranges()
     reserved_ranges = []
@@ -109,8 +112,12 @@ def test_full_circle(
     assert machine["ip_addresses"][0] in dynamic_range
     yield
 
+    deploy_osystem = (
+        machine_config.osystem if not images_to_test else images_to_test.osystem
+    )
+
     maas_api_client.deploy_machine(
-        machine, osystem=machine_config.osystem, distro_series=machine_config.osystem
+        machine, osystem=deploy_osystem, distro_series=deploy_osystem
     )
 
     machine = wait_for_machine(
@@ -126,14 +133,14 @@ def test_full_circle(
     for reserved_range in reserved_ranges:
         assert machine["ip_addresses"][0] not in reserved_range
 
-    if machine_config.osystem == "ubuntu":
+    if deploy_osystem == "ubuntu":
         stdout = ssh_execute_command(
             machine, "ubuntu", ssh_key, "cat /etc/cloud/build.info"
         )
         testlog.info(f"{machine_config.name}: {stdout}")
     yield
 
-    if machine_config.osystem == "windows":
+    if deploy_osystem == "windows":
         # We need ssh access in order to test rescue mode.
         pytest.skip("rescue mode not tested in windows.")
 
diff --git a/utils/gen_config.py b/utils/gen_config.py
index 4a8a5b5..18ad8c0 100755
--- a/utils/gen_config.py
+++ b/utils/gen_config.py
@@ -113,6 +113,20 @@ def main(argv: list[str]) -> int:
         metavar="GIT_BRANCH",
         help="Git branch to clone Ansible Playbooks from",
     )
+    image_tests_group = parser.add_argument_group(
+        "Image Tests", "Config options for Automated OS Image testing"
+    )
+    image_tests_group.add_argument(
+        "image_mapping",
+        type=argparse.FileType("r"),
+        help="Path to image_mapping.yaml",
+    )
+    image_tests_group.add_argument(
+        "--image-name",
+        action="append",
+        metavar="IMAGE_NAME",
+        help="Run tests for this image; can be repeated",
+    )
     filter_machines_group = parser.add_argument_group(
         "filters", "Use these options to filter machines to test"
     )
@@ -202,6 +216,19 @@ def main(argv: list[str]) -> int:
     else:
         config.pop("ansible-playbooks", None)
 
+    if args.image_tests:
+        config["image-tests"] = config.get("image-tests", {})
+        with args.image_mapping as fh:
+            image_cfg: dict[str, Any] = yaml.load(fh)
+        # fetch details from the image mapping cfg file
+        for image in args.image_name:
+            assert image in image_cfg.get(
+                "images", {}
+            ), "Image does not have a known mapping"
+            config["image-tests"][image] = image_cfg["images"][image]
+    else:
+        config.pop("image-tests", None)
+
     machines = config.get("machines", {})
     vms = machines.get("vms", {})
     hardware = machines.get("hardware", {})
@@ -224,11 +251,19 @@ def main(argv: list[str]) -> int:
 
     if hardware:
         if args.architecture:
+            # if running custom image tests, only use compatible machines
+            target_arches = (
+                args.architecture
+                if not args.image_tests
+                else [
+                    image["architecture"] for _, image in config["image-tests"].items()
+                ]
+            )
             # Filter out machines with architectures not matching specified ones.
             machines["hardware"] = {
                 name: details
                 for name, details in hardware.items()
-                if details.get("architecture", "amd64") in args.architecture
+                if details.get("architecture", "amd64") in target_arches
             }
 
         if args.machine:

Follow ups