diff options
| -rw-r--r-- | recipes-extended/images/README-xen.md | 224 | ||||
| -rw-r--r-- | tests/test_xen_guest_bundle.py | 361 |
2 files changed, 490 insertions, 95 deletions
diff --git a/recipes-extended/images/README-xen.md b/recipes-extended/images/README-xen.md index 82d72364..f16aa743 100644 --- a/recipes-extended/images/README-xen.md +++ b/recipes-extended/images/README-xen.md | |||
| @@ -20,154 +20,188 @@ to differentiate). | |||
| 20 | 20 | ||
| 21 | It creates tarballs, ext4 and qcow images for testing purposes. | 21 | It creates tarballs, ext4 and qcow images for testing purposes. |
| 22 | 22 | ||
| 23 | bundling | 23 | Bundling |
| 24 | -------- | 24 | -------- |
| 25 | 25 | ||
| 26 | Guests can be bundled automatically through the following mechanisms: | 26 | There are two ways to bundle Xen guests into a Dom0 host image: |
| 27 | 27 | ||
| 28 | - via the variable XEN_BUNDLED_GUESTS | 28 | | Use Case | `BUNDLED_XEN_GUESTS` | Bundle Recipe | |
| 29 | - via a xen configuration file in the deploy directory of the format | 29 | |---|---|---| |
| 30 | xen-guest-bundle-*.cfg | 30 | | Simple: guests in one host image | recommended | overkill | |
| 31 | | Reuse guests across multiple host images | repetitive | recommended | | ||
| 32 | | Package versioning and dependencies | not supported | supported | | ||
| 33 | | Distribute pre-built guest sets | not supported | supported | | ||
| 31 | 34 | ||
| 32 | The guests can be built via OE, or be 3rd party guests. They just | 35 | ### Variable-driven (BUNDLED_XEN_GUESTS) |
| 33 | must be in the deploy directory so they can be copied into the rootfs | ||
| 34 | of the xen host image | ||
| 35 | 36 | ||
| 36 | Type 1) XEN_BUNDLED_GUESTS | 37 | Guests can be bundled into the host image automatically using |
| 38 | `xen-guest-cross-install.bbclass` (inherited by xen-image-minimal). | ||
| 37 | 39 | ||
| 38 | If XEN_BUNDLED_GUESTS is used, it is simply a colon separated list of | 40 | Set `BUNDLED_XEN_GUESTS` in local.conf or the image recipe: |
| 39 | rootfs:kernels. Normal variable rules apply, so it can be set in a | ||
| 40 | local.conf, or in a bbappend to the image recipe. | ||
| 41 | 41 | ||
| 42 | An example would be: | 42 | BUNDLED_XEN_GUESTS = "xen-guest-image-minimal:autostart" |
| 43 | 43 | ||
| 44 | XEN_BUNDLED_GUESTS = "xen-guest-image-minimal-qemuarm64.rootfs.ext4:Image" | 44 | Each entry is a recipe name with optional tags: |
| 45 | 45 | ||
| 46 | These point at symlinks created in the image deploy directory, or they | 46 | recipe-name[:autostart][:external] |
| 47 | can be specific images/kernels without the symlink. | ||
| 48 | 47 | ||
| 49 | Type 2) A Xen guest configuration file | 48 | - recipe-name: Yocto image recipe that produces the guest rootfs |
| 49 | - autostart: Creates symlink in /etc/xen/auto/ for xendomains | ||
| 50 | - external: Skip dependency generation (3rd-party guest) | ||
| 50 | 51 | ||
| 51 | If xen guest configuration files are found in the deploy directories | 52 | Examples: |
| 52 | the kernel and disk information contained within them will be processed | ||
| 53 | and modified for the xen host. The kernel and guest image will be | ||
| 54 | copied to the appropriate location, and the config made to match. | ||
| 55 | 53 | ||
| 56 | These files following the naming convention: xen-guest-bundle*.cfg | 54 | # Single guest with autostart (default recommendation) |
| 55 | BUNDLED_XEN_GUESTS = "xen-guest-image-minimal:autostart" | ||
| 57 | 56 | ||
| 58 | Guests of type #1 generate a configuration file that is picked up as | 57 | # Guest without autostart |
| 59 | type #2. | 58 | BUNDLED_XEN_GUESTS = "xen-guest-image-minimal" |
| 60 | 59 | ||
| 61 | An example config file follows: | 60 | # External/3rd-party guest (no build dependency) |
| 61 | BUNDLED_XEN_GUESTS = "my-vendor-guest:external" | ||
| 62 | 62 | ||
| 63 | name = "xen-guest" | 63 | Per-guest configuration via varflags: |
| 64 | memory = 512 | ||
| 65 | vcpus = 1 | ||
| 66 | disk = ['file:xen-guest-image-minimal-qemuarm64.rootfs.ext4,xvda,rw'] | ||
| 67 | vif = ['bridge=xenbr0'] | ||
| 68 | kernel = "Image" | ||
| 69 | extra = "root=/dev/xvda ro console=hvc0 ip=dhcp" | ||
| 70 | 64 | ||
| 71 | It should also be noted that when a xen-guest-image-minimal is built | 65 | XEN_GUEST_MEMORY[xen-guest-image-minimal] = "1024" |
| 72 | with the XEN_GUEST_AUTO_BUNDLE varaible set to True, a configuration | 66 | XEN_GUEST_VCPUS[xen-guest-image-minimal] = "2" |
| 73 | file for type #2 will be generated and the guest bundled automatically | 67 | XEN_GUEST_VIF[xen-guest-image-minimal] = "bridge=xenbr0" |
| 74 | when the host image is built. | 68 | XEN_GUEST_EXTRA[xen-guest-image-minimal] = "root=/dev/xvda ro console=hvc0 ip=dhcp" |
| 69 | |||
| 70 | Custom config file (replaces auto-generation): | ||
| 71 | |||
| 72 | SRC_URI += "file://my-custom-guest.cfg" | ||
| 73 | BUNDLED_XEN_GUESTS = "xen-guest-image-minimal:autostart" | ||
| 74 | XEN_GUEST_CONFIG_FILE[xen-guest-image-minimal] = "${UNPACKDIR}/my-custom-guest.cfg" | ||
| 75 | |||
| 76 | Explicit rootfs/kernel for external guests: | ||
| 77 | |||
| 78 | XEN_GUEST_ROOTFS[my-vendor-guest] = "vendor-rootfs.ext4" | ||
| 79 | XEN_GUEST_KERNEL[my-vendor-guest] = "vendor-kernel" | ||
| 80 | |||
| 81 | ### Package-based (xen-guest-bundle.bbclass) | ||
| 82 | |||
| 83 | For reusable guest sets, create a bundle recipe that inherits | ||
| 84 | `xen-guest-bundle`: | ||
| 85 | |||
| 86 | # recipes-extended/xen-guest-bundles/my-guests_1.0.bb | ||
| 87 | inherit xen-guest-bundle | ||
| 88 | |||
| 89 | XEN_GUEST_BUNDLES = "xen-guest-image-minimal:autostart" | ||
| 90 | XEN_GUEST_MEMORY[xen-guest-image-minimal] = "1024" | ||
| 91 | |||
| 92 | Then install the bundle in the host image: | ||
| 93 | |||
| 94 | IMAGE_INSTALL:append:pn-xen-image-minimal = " my-guests" | ||
| 95 | |||
| 96 | The bundle package includes rootfs, kernel, and config files. At | ||
| 97 | image time, `merge_installed_xen_bundles()` deploys them to the | ||
| 98 | same target locations as the variable-driven path. | ||
| 99 | |||
| 100 | Custom config files work the same way via SRC_URI + varflag: | ||
| 101 | |||
| 102 | SRC_URI += "file://my-custom-guest.cfg" | ||
| 103 | XEN_GUEST_CONFIG_FILE[xen-guest-image-minimal] = "${UNPACKDIR}/my-custom-guest.cfg" | ||
| 104 | |||
| 105 | See `example-xen-guest-bundle_1.0.bb` for a complete example. | ||
| 106 | |||
| 107 | ### 3rd-party guest import | ||
| 108 | |||
| 109 | The import system converts fetched source formats (tarballs, qcow2 images, | ||
| 110 | etc.) into Xen-ready disk images at build time. This is for guests that | ||
| 111 | are not built by Yocto (e.g., Alpine minirootfs, Debian cloud images). | ||
| 112 | |||
| 113 | Per-guest varflags control the import: | ||
| 114 | |||
| 115 | XEN_GUEST_SOURCE_TYPE[guest] = "rootfs_dir" # import handler type | ||
| 116 | XEN_GUEST_SOURCE_FILE[guest] = "alpine-rootfs" # file/dir in UNPACKDIR | ||
| 117 | XEN_GUEST_IMAGE_SIZE[guest] = "128" # target image size in MB | ||
| 118 | |||
| 119 | Built-in import types: | ||
| 120 | |||
| 121 | | Type | Input | Output | Tool | | ||
| 122 | |---|---|---|---| | ||
| 123 | | `rootfs_dir` | Extracted rootfs directory | ext4 image | `mkfs.ext4 -F -d` | | ||
| 124 | | `qcow2` | QCOW2 disk image | raw image | `qemu-img convert` | | ||
| 125 | | `ext4` | ext4 image file | ext4 (copy) | `cp` | | ||
| 126 | | `raw` | raw disk image | raw (copy) | `cp` | | ||
| 127 | |||
| 128 | Native tool dependencies are resolved automatically at parse time. | ||
| 129 | |||
| 130 | Kernel modes (per-guest via `XEN_GUEST_KERNEL` varflag): | ||
| 131 | |||
| 132 | - (not set): Shared host kernel from DEPLOY_DIR_IMAGE | ||
| 133 | - `"path"`: Custom kernel from UNPACKDIR or DEPLOY_DIR_IMAGE | ||
| 134 | - `"none"`: HVM guest, no kernel (omits kernel= from config) | ||
| 135 | |||
| 136 | Alpine example (`alpine-xen-guest-bundle_3.23.bb`): | ||
| 137 | |||
| 138 | inherit xen-guest-bundle | ||
| 139 | |||
| 140 | SRC_URI = "https://...alpine-minirootfs-${ALPINE_VERSION}-${ALPINE_ARCH}.tar.gz;subdir=alpine-rootfs" | ||
| 141 | |||
| 142 | XEN_GUEST_BUNDLES = "alpine:autostart:external" | ||
| 143 | XEN_GUEST_SOURCE_TYPE[alpine] = "rootfs_dir" | ||
| 144 | XEN_GUEST_SOURCE_FILE[alpine] = "alpine-rootfs" | ||
| 145 | XEN_GUEST_IMAGE_SIZE[alpine] = "128" | ||
| 146 | XEN_GUEST_MEMORY[alpine] = "256" | ||
| 147 | XEN_GUEST_EXTRA[alpine] = "root=/dev/xvda ro console=hvc0" | ||
| 148 | |||
| 149 | Adding custom import types: define a shell function | ||
| 150 | `xen_guest_import_<type>(source_path, output_path, size_mb)` in a | ||
| 151 | bbclass, recipe, or bbappend and set the corresponding | ||
| 152 | `XEN_GUEST_IMPORT_DEPENDS_<type>` variable for native tool dependencies. | ||
| 153 | |||
| 154 | Target layout | ||
| 155 | ------------- | ||
| 75 | 156 | ||
| 76 | kernel and rootfs are copied to the target in /var/lib/xen/images/ | 157 | kernel and rootfs are copied to the target in /var/lib/xen/images/ |
| 77 | 158 | ||
| 78 | configuration files are copied to: /etc/xen | 159 | configuration files are copied to: /etc/xen |
| 79 | 160 | ||
| 80 | Guests can be launched after boot with: xl create -c /etc/xen/<config file> | 161 | autostart symlinks are created in: /etc/xen/auto/ |
| 162 | |||
| 163 | Guests can be launched after boot with: xl create -c /etc/xen/<guest>.cfg | ||
| 81 | 164 | ||
| 82 | Build and boot | 165 | Build and boot |
| 83 | -------------- | 166 | -------------- |
| 84 | 167 | ||
| 85 | Using a reference qmeuarm64 MACHINE, the following are the commands | 168 | Using a reference qemuarm64 MACHINE, the following are the commands |
| 86 | to build and boot a guest. | 169 | to build and boot a guest. |
| 87 | 170 | ||
| 88 | local.conf contains: | 171 | local.conf contains: |
| 89 | 172 | ||
| 90 | XEN_BUNDLED_GUESTS = "xen-guest-image-minimal-qemuarm64.rootfs.ext4:Image" | 173 | BUNDLED_XEN_GUESTS = "xen-guest-image-minimal:autostart" |
| 91 | 174 | ||
| 92 | % bitbake xen-guest-image-minimal | 175 | % bitbake xen-guest-image-minimal |
| 93 | % bitbake xen-image-minimal | 176 | % bitbake xen-image-minimal |
| 94 | 177 | ||
| 95 | % runqemu qemuarm64 nographic slirp qemuparams="-m 4096" tmp/deploy/images/qemuarm64/xen-image-minimal-qemuarm64.rootfs.ext4 | 178 | % runqemu qemuarm64 nographic slirp qemuparams="-m 4096" |
| 96 | 179 | ||
| 97 | Poky (Yocto Project Reference Distro) 5.1 qemuarm64 hvc0 | 180 | Poky (Yocto Project Reference Distro) 5.1 qemuarm64 hvc0 |
| 98 | 181 | ||
| 99 | qemuarm64 login: root | 182 | qemuarm64 login: root |
| 100 | 183 | ||
| 101 | WARNING: Poky is a reference Yocto Project distribution that should be used for | ||
| 102 | testing and development purposes only. It is recommended that you create your | ||
| 103 | own distribution for production use. | ||
| 104 | |||
| 105 | root@qemuarm64:~# uname -a | ||
| 106 | Linux qemuarm64 6.10.11-yocto-standard #1 SMP PREEMPT Fri Sep 20 22:32:26 UTC 2024 aarch64 GNU/Linux | ||
| 107 | root@qemuarm64:~# ls /etc/xen/ | 184 | root@qemuarm64:~# ls /etc/xen/ |
| 108 | auto | 185 | auto |
| 109 | cpupool | 186 | cpupool |
| 110 | scripts | 187 | scripts |
| 111 | xen-guest-bundle-xen-guest-image-minimal-qemuarm64--20241112174803.cfg | 188 | xen-guest-image-minimal.cfg |
| 112 | xl.conf | 189 | xl.conf |
| 113 | root@qemuarm64:~# ls /var/lib/xen/images/ | 190 | root@qemuarm64:~# ls /var/lib/xen/images/ |
| 114 | Image--6.10.11+git0+4bf82718cf_6c956b2ea6-r0-qemuarm64-20241018190311.bin | 191 | Image--6.10.11+git0+4bf82718cf_6c956b2ea6-r0-qemuarm64-20241018190311.bin |
| 115 | xen-guest-image-minimal-qemuarm64.rootfs-20241111222814.ext4 | 192 | xen-guest-image-minimal-qemuarm64-20241111222814.ext4 |
| 116 | |||
| 117 | root@qemuarm64:~# ip a s | ||
| 118 | 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000 | ||
| 119 | link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 | ||
| 120 | inet 127.0.0.1/8 scope host lo | ||
| 121 | valid_lft forever preferred_lft forever | ||
| 122 | inet6 ::1/128 scope host noprefixroute | ||
| 123 | valid_lft forever preferred_lft forever | ||
| 124 | 2: enp0s1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master xenbr0 qlen 1000 | ||
| 125 | link/ether 52:54:00:12:35:02 brd ff:ff:ff:ff:ff:ff | ||
| 126 | 3: sit0@NONE: <NOARP> mtu 1480 qdisc noop qlen 1000 | ||
| 127 | link/sit 0.0.0.0 brd 0.0.0.0 | ||
| 128 | 4: xenbr0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue qlen 1000 | ||
| 129 | link/ether ee:e4:a8:24:24:e7 brd ff:ff:ff:ff:ff:ff | ||
| 130 | inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic xenbr0 | ||
| 131 | valid_lft 86354sec preferred_lft 86354sec | ||
| 132 | inet6 fec0::ece4:a8ff:fe24:24e7/64 scope site dynamic noprefixroute flags 100 | ||
| 133 | valid_lft 86356sec preferred_lft 14356sec | ||
| 134 | inet6 fe80::ece4:a8ff:fe24:24e7/64 scope link | ||
| 135 | valid_lft forever preferred_lft forever | ||
| 136 | |||
| 137 | root@qemuarm64:~# xl create -c /etc/xen/xen-guest-bundle-xen-guest-image-minimal-qemuarm64--20241112174803.cfg | ||
| 138 | 193 | ||
| 139 | qemuarm64 login: root | 194 | root@qemuarm64:~# xl create -c /etc/xen/xen-guest-image-minimal.cfg |
| 140 | 195 | ||
| 141 | WARNING: Poky is a reference Yocto Project distribution that should be used for | 196 | qemuarm64 login: root |
| 142 | testing and development purposes only. It is recommended that you create your | ||
| 143 | own distribution for production use. | ||
| 144 | 197 | ||
| 145 | root@qemuarm64:~# uname -a | 198 | root@qemuarm64:~# uname -a |
| 146 | Linux qemuarm64 6.10.11-yocto-standard #1 SMP PREEMPT Fri Sep 20 22:32:26 UTC 2024 aarch64 GNU/Linux | 199 | Linux qemuarm64 6.10.11-yocto-standard #1 SMP PREEMPT Fri Sep 20 22:32:26 UTC 2024 aarch64 GNU/Linux |
| 147 | 200 | ||
| 148 | root@qemuarm64:~# wget example.com | ||
| 149 | Connecting to example.com (93.184.215.14:80) | ||
| 150 | wget: can't open 'index.html': File exists | ||
| 151 | root@qemuarm64:~# rm index.html | ||
| 152 | root@qemuarm64:~# wget example.com | ||
| 153 | Connecting to example.com (93.184.215.14:80) | ||
| 154 | saving to 'index.html' | ||
| 155 | index.html 100% |********************************| 1256 0:00:00 ETA | ||
| 156 | 'index.html' saved | ||
| 157 | |||
| 158 | From the host: | 201 | From the host: |
| 159 | 202 | ||
| 160 | Connection to 127.0.0.1 closed. | ||
| 161 | build4 [/home/bruc.../qemuarm64]> ssh -p 2222 root@127.0.0.1 | ||
| 162 | Last login: Tue Nov 12 20:42:57 2024 from 10.0.2.2 | ||
| 163 | |||
| 164 | WARNING: Poky is a reference Yocto Project distribution that should be used for | ||
| 165 | testing and development purposes only. It is recommended that you create your | ||
| 166 | own distribution for production use. | ||
| 167 | |||
| 168 | root@qemuarm64:~# xl list | 203 | root@qemuarm64:~# xl list |
| 169 | Name ID Mem VCPUs State Time(s) | 204 | Name ID Mem VCPUs State Time(s) |
| 170 | Domain-0 0 192 4 r----- 696.2 | 205 | Domain-0 0 192 4 r----- 696.2 |
| 171 | xen-guest 1 512 1 -b---- 153.0 | 206 | xen-guest-image-minimal 1 512 1 -b---- 153.0 |
| 172 | root@qemuarm64:~# xl destroy xen-guest | 207 | root@qemuarm64:~# xl destroy xen-guest-image-minimal |
| 173 | |||
diff --git a/tests/test_xen_guest_bundle.py b/tests/test_xen_guest_bundle.py new file mode 100644 index 00000000..2035a5f9 --- /dev/null +++ b/tests/test_xen_guest_bundle.py | |||
| @@ -0,0 +1,361 @@ | |||
| 1 | # SPDX-FileCopyrightText: Copyright (C) 2025 Bruce Ashfield | ||
| 2 | # | ||
| 3 | # SPDX-License-Identifier: MIT | ||
| 4 | """ | ||
| 5 | Tests for xen-guest-bundle.bbclass - Xen guest bundling system. | ||
| 6 | |||
| 7 | These tests verify: | ||
| 8 | - bbclass file structure and syntax | ||
| 9 | - Import handler definitions | ||
| 10 | - Parse-time logic (__anonymous) | ||
| 11 | - Alpine example recipe structure | ||
| 12 | - Build tests (slow, require configured build environment) | ||
| 13 | |||
| 14 | Run with: | ||
| 15 | pytest tests/test_xen_guest_bundle.py -v | ||
| 16 | |||
| 17 | Run build tests (requires configured Yocto build): | ||
| 18 | pytest tests/test_xen_guest_bundle.py -v -m slow --machine qemuarm64 | ||
| 19 | |||
| 20 | Environment variables: | ||
| 21 | POKY_DIR: Path to poky directory (default: /opt/bruce/poky) | ||
| 22 | BUILD_DIR: Path to build directory (default: $POKY_DIR/build) | ||
| 23 | MACHINE: Target machine (default: qemux86-64) | ||
| 24 | """ | ||
| 25 | |||
| 26 | import re | ||
| 27 | import pytest | ||
| 28 | from pathlib import Path | ||
| 29 | |||
| 30 | |||
| 31 | # Note: Command line options (--poky-dir, --build-dir, --machine) | ||
| 32 | # are defined in conftest.py | ||
| 33 | |||
| 34 | |||
| 35 | @pytest.fixture(scope="module") | ||
| 36 | def poky_dir(request): | ||
| 37 | """Path to poky directory.""" | ||
| 38 | path = Path(request.config.getoption("--poky-dir")) | ||
| 39 | if not path.exists(): | ||
| 40 | pytest.skip(f"Poky directory not found: {path}") | ||
| 41 | return path | ||
| 42 | |||
| 43 | |||
| 44 | @pytest.fixture(scope="module") | ||
| 45 | def meta_virt_dir(poky_dir): | ||
| 46 | """Path to meta-virtualization layer.""" | ||
| 47 | path = poky_dir / "meta-virtualization" | ||
| 48 | if not path.exists(): | ||
| 49 | pytest.skip(f"meta-virtualization not found: {path}") | ||
| 50 | return path | ||
| 51 | |||
| 52 | |||
| 53 | @pytest.fixture(scope="module") | ||
| 54 | def bbclass_content(meta_virt_dir): | ||
| 55 | """Content of xen-guest-bundle.bbclass.""" | ||
| 56 | path = meta_virt_dir / "classes" / "xen-guest-bundle.bbclass" | ||
| 57 | if not path.exists(): | ||
| 58 | pytest.skip(f"bbclass not found: {path}") | ||
| 59 | return path.read_text() | ||
| 60 | |||
| 61 | |||
| 62 | @pytest.fixture(scope="module") | ||
| 63 | def alpine_recipe_content(meta_virt_dir): | ||
| 64 | """Content of alpine-xen-guest-bundle recipe.""" | ||
| 65 | recipes = list((meta_virt_dir / "recipes-extended" / "xen-guest-bundles").glob( | ||
| 66 | "alpine-xen-guest-bundle_*.bb")) | ||
| 67 | if not recipes: | ||
| 68 | pytest.skip("Alpine guest bundle recipe not found") | ||
| 69 | return recipes[0].read_text() | ||
| 70 | |||
| 71 | |||
| 72 | # ============================================================================ | ||
| 73 | # bbclass structure tests | ||
| 74 | # ============================================================================ | ||
| 75 | |||
| 76 | class TestXenGuestBundleClass: | ||
| 77 | """Test xen-guest-bundle.bbclass structure and syntax.""" | ||
| 78 | |||
| 79 | def test_class_exists(self, meta_virt_dir): | ||
| 80 | """Test that the bbclass file exists.""" | ||
| 81 | path = meta_virt_dir / "classes" / "xen-guest-bundle.bbclass" | ||
| 82 | assert path.exists(), f"bbclass not found: {path}" | ||
| 83 | |||
| 84 | def test_spdx_header(self, bbclass_content): | ||
| 85 | """Test SPDX license header is present.""" | ||
| 86 | assert "SPDX-License-Identifier: MIT" in bbclass_content | ||
| 87 | |||
| 88 | def test_default_variables(self, bbclass_content): | ||
| 89 | """Test that expected default variables are defined.""" | ||
| 90 | defaults = [ | ||
| 91 | "XEN_GUEST_BUNDLES", | ||
| 92 | "XEN_GUEST_IMAGE_FSTYPE", | ||
| 93 | "XEN_GUEST_MEMORY_DEFAULT", | ||
| 94 | "XEN_GUEST_VCPUS_DEFAULT", | ||
| 95 | "XEN_GUEST_VIF_DEFAULT", | ||
| 96 | "XEN_GUEST_EXTRA_DEFAULT", | ||
| 97 | "XEN_GUEST_DISK_DEVICE_DEFAULT", | ||
| 98 | ] | ||
| 99 | for var in defaults: | ||
| 100 | assert var in bbclass_content, f"Default variable {var} not found" | ||
| 101 | |||
| 102 | def test_anonymous_function(self, bbclass_content): | ||
| 103 | """Test that __anonymous() is defined.""" | ||
| 104 | assert "python __anonymous()" in bbclass_content | ||
| 105 | |||
| 106 | def test_do_compile_defined(self, bbclass_content): | ||
| 107 | """Test that do_compile is defined.""" | ||
| 108 | assert "do_compile()" in bbclass_content | ||
| 109 | |||
| 110 | def test_do_install_defined(self, bbclass_content): | ||
| 111 | """Test that do_install is defined.""" | ||
| 112 | assert "do_install()" in bbclass_content | ||
| 113 | |||
| 114 | def test_resolve_bundle_rootfs(self, bbclass_content): | ||
| 115 | """Test rootfs resolver function exists.""" | ||
| 116 | assert "resolve_bundle_rootfs()" in bbclass_content | ||
| 117 | |||
| 118 | def test_resolve_bundle_kernel(self, bbclass_content): | ||
| 119 | """Test kernel resolver function exists.""" | ||
| 120 | assert "resolve_bundle_kernel()" in bbclass_content | ||
| 121 | |||
| 122 | def test_generate_bundle_config(self, bbclass_content): | ||
| 123 | """Test config generator function exists.""" | ||
| 124 | assert "generate_bundle_config()" in bbclass_content | ||
| 125 | |||
| 126 | def test_files_variable(self, bbclass_content): | ||
| 127 | """Test FILES variable is set.""" | ||
| 128 | assert "FILES:${PN}" in bbclass_content | ||
| 129 | assert "xen-guest-bundles" in bbclass_content | ||
| 130 | |||
| 131 | def test_insane_skip(self, bbclass_content): | ||
| 132 | """Test INSANE_SKIP for binary images.""" | ||
| 133 | assert "INSANE_SKIP" in bbclass_content | ||
| 134 | assert "buildpaths" in bbclass_content | ||
| 135 | |||
| 136 | |||
| 137 | # ============================================================================ | ||
| 138 | # Import system tests | ||
| 139 | # ============================================================================ | ||
| 140 | |||
| 141 | class TestImportSystem: | ||
| 142 | """Test import system for 3rd-party guests.""" | ||
| 143 | |||
| 144 | def test_import_default_variables(self, bbclass_content): | ||
| 145 | """Test import-related default variables.""" | ||
| 146 | assert "XEN_GUEST_IMAGE_SIZE_DEFAULT" in bbclass_content | ||
| 147 | assert "XEN_GUEST_IMPORT_DEPENDS_rootfs_dir" in bbclass_content | ||
| 148 | assert "XEN_GUEST_IMPORT_DEPENDS_qcow2" in bbclass_content | ||
| 149 | assert "XEN_GUEST_IMPORT_DEPENDS_ext4" in bbclass_content | ||
| 150 | assert "XEN_GUEST_IMPORT_DEPENDS_raw" in bbclass_content | ||
| 151 | |||
| 152 | def test_import_depends_rootfs_dir(self, bbclass_content): | ||
| 153 | """Test rootfs_dir depends on e2fsprogs-native.""" | ||
| 154 | match = re.search( | ||
| 155 | r'XEN_GUEST_IMPORT_DEPENDS_rootfs_dir\s*=\s*"([^"]*)"', | ||
| 156 | bbclass_content) | ||
| 157 | assert match, "rootfs_dir depends not found" | ||
| 158 | assert "e2fsprogs-native" in match.group(1) | ||
| 159 | |||
| 160 | def test_import_depends_qcow2(self, bbclass_content): | ||
| 161 | """Test qcow2 depends on qemu-system-native.""" | ||
| 162 | match = re.search( | ||
| 163 | r'XEN_GUEST_IMPORT_DEPENDS_qcow2\s*=\s*"([^"]*)"', | ||
| 164 | bbclass_content) | ||
| 165 | assert match, "qcow2 depends not found" | ||
| 166 | assert "qemu-system-native" in match.group(1) | ||
| 167 | |||
| 168 | def test_import_handler_rootfs_dir(self, bbclass_content): | ||
| 169 | """Test rootfs_dir import handler exists.""" | ||
| 170 | assert "xen_guest_import_rootfs_dir()" in bbclass_content | ||
| 171 | assert "mkfs.ext4" in bbclass_content | ||
| 172 | |||
| 173 | def test_import_handler_qcow2(self, bbclass_content): | ||
| 174 | """Test qcow2 import handler exists.""" | ||
| 175 | assert "xen_guest_import_qcow2()" in bbclass_content | ||
| 176 | assert "qemu-img convert" in bbclass_content | ||
| 177 | |||
| 178 | def test_import_handler_ext4(self, bbclass_content): | ||
| 179 | """Test ext4 import handler exists.""" | ||
| 180 | assert "xen_guest_import_ext4()" in bbclass_content | ||
| 181 | |||
| 182 | def test_import_handler_raw(self, bbclass_content): | ||
| 183 | """Test raw import handler exists.""" | ||
| 184 | assert "xen_guest_import_raw()" in bbclass_content | ||
| 185 | |||
| 186 | def test_resolve_import_source(self, bbclass_content): | ||
| 187 | """Test import source resolver exists.""" | ||
| 188 | assert "resolve_import_source()" in bbclass_content | ||
| 189 | assert "_XEN_GUEST_IMPORT_MAP" in bbclass_content | ||
| 190 | |||
| 191 | def test_static_dispatch_in_do_compile(self, bbclass_content): | ||
| 192 | """Test that import dispatch uses static case statement.""" | ||
| 193 | # BitBake needs static function references to include them | ||
| 194 | assert "case \"$import_type\" in" in bbclass_content | ||
| 195 | assert "xen_guest_import_rootfs_dir " in bbclass_content | ||
| 196 | assert "xen_guest_import_qcow2 " in bbclass_content | ||
| 197 | |||
| 198 | def test_fakeroot_for_rootfs_dir(self, bbclass_content): | ||
| 199 | """Test that rootfs_dir type triggers fakeroot.""" | ||
| 200 | assert "fakeroot" in bbclass_content | ||
| 201 | assert "rootfs_dir" in bbclass_content | ||
| 202 | |||
| 203 | |||
| 204 | # ============================================================================ | ||
| 205 | # Kernel mode tests | ||
| 206 | # ============================================================================ | ||
| 207 | |||
| 208 | class TestKernelModes: | ||
| 209 | """Test three kernel modes: shared, custom, HVM/none.""" | ||
| 210 | |||
| 211 | def test_hvm_mode_documented(self, bbclass_content): | ||
| 212 | """Test HVM mode (kernel=none) is supported.""" | ||
| 213 | assert '"none"' in bbclass_content or "'none'" in bbclass_content | ||
| 214 | assert "HVM" in bbclass_content | ||
| 215 | |||
| 216 | def test_kernel_unpackdir_check(self, bbclass_content): | ||
| 217 | """Test kernel resolver checks UNPACKDIR.""" | ||
| 218 | assert "UNPACKDIR" in bbclass_content | ||
| 219 | |||
| 220 | def test_config_omits_kernel_for_hvm(self, bbclass_content): | ||
| 221 | """Test generate_bundle_config omits kernel for HVM.""" | ||
| 222 | # Should have conditional kernel output | ||
| 223 | assert 'if [ -n "$kernel_basename" ]' in bbclass_content | ||
| 224 | |||
| 225 | def test_shared_kernel_dependency(self, bbclass_content): | ||
| 226 | """Test virtual/kernel dependency for shared kernel.""" | ||
| 227 | assert "virtual/kernel:do_deploy" in bbclass_content | ||
| 228 | |||
| 229 | |||
| 230 | # ============================================================================ | ||
| 231 | # License warning tests | ||
| 232 | # ============================================================================ | ||
| 233 | |||
| 234 | class TestLicenseWarning: | ||
| 235 | """Test external guest license warning.""" | ||
| 236 | |||
| 237 | def test_external_names_variable(self, bbclass_content): | ||
| 238 | """Test _XEN_GUEST_EXTERNAL_NAMES is set for external guests.""" | ||
| 239 | assert "_XEN_GUEST_EXTERNAL_NAMES" in bbclass_content | ||
| 240 | |||
| 241 | def test_license_warn_prefunc(self, bbclass_content): | ||
| 242 | """Test license warning is a prefunc on do_compile.""" | ||
| 243 | assert "xen_guest_external_license_warn" in bbclass_content | ||
| 244 | assert "do_compile[prefuncs]" in bbclass_content | ||
| 245 | |||
| 246 | def test_license_warn_content(self, bbclass_content): | ||
| 247 | """Test license warning message content.""" | ||
| 248 | assert "rights to redistribute" in bbclass_content | ||
| 249 | assert "license terms" in bbclass_content | ||
| 250 | |||
| 251 | def test_license_warn_is_python(self, bbclass_content): | ||
| 252 | """Test license warning is a python function (runs once at task time).""" | ||
| 253 | assert "python xen_guest_external_license_warn()" in bbclass_content | ||
| 254 | |||
| 255 | |||
| 256 | # ============================================================================ | ||
| 257 | # Alpine recipe tests | ||
| 258 | # ============================================================================ | ||
| 259 | |||
| 260 | class TestAlpineRecipe: | ||
| 261 | """Test alpine-xen-guest-bundle recipe structure.""" | ||
| 262 | |||
| 263 | def test_recipe_exists(self, meta_virt_dir): | ||
| 264 | """Test that Alpine recipe exists.""" | ||
| 265 | recipes = list((meta_virt_dir / "recipes-extended" / "xen-guest-bundles").glob( | ||
| 266 | "alpine-xen-guest-bundle_*.bb")) | ||
| 267 | assert len(recipes) > 0, "Alpine guest bundle recipe not found" | ||
| 268 | |||
| 269 | def test_inherits_xen_guest_bundle(self, alpine_recipe_content): | ||
| 270 | """Test recipe inherits xen-guest-bundle.""" | ||
| 271 | assert "inherit xen-guest-bundle" in alpine_recipe_content | ||
| 272 | |||
| 273 | def test_license(self, alpine_recipe_content): | ||
| 274 | """Test recipe has license.""" | ||
| 275 | assert 'LICENSE = "MIT"' in alpine_recipe_content | ||
| 276 | assert "LIC_FILES_CHKSUM" in alpine_recipe_content | ||
| 277 | |||
| 278 | def test_src_uri(self, alpine_recipe_content): | ||
| 279 | """Test SRC_URI fetches Alpine minirootfs.""" | ||
| 280 | assert "dl-cdn.alpinelinux.org" in alpine_recipe_content | ||
| 281 | assert "alpine-minirootfs" in alpine_recipe_content | ||
| 282 | assert "subdir=alpine-rootfs" in alpine_recipe_content | ||
| 283 | |||
| 284 | def test_sha256sum(self, alpine_recipe_content): | ||
| 285 | """Test sha256sum is set (not placeholder).""" | ||
| 286 | match = re.search(r'SRC_URI\[sha256sum\]\s*=\s*"([^"]*)"', | ||
| 287 | alpine_recipe_content) | ||
| 288 | assert match, "sha256sum not found" | ||
| 289 | sha = match.group(1) | ||
| 290 | assert len(sha) == 64, f"sha256sum wrong length: {len(sha)}" | ||
| 291 | assert sha != "x" * 64, "sha256sum is still placeholder" | ||
| 292 | |||
| 293 | def test_guest_bundles(self, alpine_recipe_content): | ||
| 294 | """Test XEN_GUEST_BUNDLES is set.""" | ||
| 295 | assert 'XEN_GUEST_BUNDLES = "alpine:autostart:external"' in alpine_recipe_content | ||
| 296 | |||
| 297 | def test_import_source_type(self, alpine_recipe_content): | ||
| 298 | """Test import source type is rootfs_dir.""" | ||
| 299 | assert 'XEN_GUEST_SOURCE_TYPE[alpine] = "rootfs_dir"' in alpine_recipe_content | ||
| 300 | |||
| 301 | def test_import_source_file(self, alpine_recipe_content): | ||
| 302 | """Test import source file matches SRC_URI subdir.""" | ||
| 303 | assert 'XEN_GUEST_SOURCE_FILE[alpine] = "alpine-rootfs"' in alpine_recipe_content | ||
| 304 | |||
| 305 | def test_image_size(self, alpine_recipe_content): | ||
| 306 | """Test image size is set.""" | ||
| 307 | assert 'XEN_GUEST_IMAGE_SIZE[alpine]' in alpine_recipe_content | ||
| 308 | |||
| 309 | def test_guest_memory(self, alpine_recipe_content): | ||
| 310 | """Test guest memory is set.""" | ||
| 311 | assert 'XEN_GUEST_MEMORY[alpine]' in alpine_recipe_content | ||
| 312 | |||
| 313 | def test_guest_extra(self, alpine_recipe_content): | ||
| 314 | """Test guest extra args include console.""" | ||
| 315 | assert 'XEN_GUEST_EXTRA[alpine]' in alpine_recipe_content | ||
| 316 | assert "console=hvc0" in alpine_recipe_content | ||
| 317 | |||
| 318 | def test_multiarch_support(self, alpine_recipe_content): | ||
| 319 | """Test recipe supports multiple architectures.""" | ||
| 320 | assert "ALPINE_ARCH" in alpine_recipe_content | ||
| 321 | assert "aarch64" in alpine_recipe_content | ||
| 322 | assert "x86_64" in alpine_recipe_content | ||
| 323 | |||
| 324 | |||
| 325 | # ============================================================================ | ||
| 326 | # README tests | ||
| 327 | # ============================================================================ | ||
| 328 | |||
| 329 | class TestReadme: | ||
| 330 | """Test README-xen.md documentation.""" | ||
| 331 | |||
| 332 | @pytest.fixture(scope="class") | ||
| 333 | def readme_content(self, meta_virt_dir): | ||
| 334 | path = meta_virt_dir / "recipes-extended" / "images" / "README-xen.md" | ||
| 335 | if not path.exists(): | ||
| 336 | pytest.skip("README-xen.md not found") | ||
| 337 | return path.read_text() | ||
| 338 | |||
| 339 | def test_import_section_exists(self, readme_content): | ||
| 340 | """Test 3rd-party import section exists.""" | ||
| 341 | assert "3rd-party guest import" in readme_content | ||
| 342 | |||
| 343 | def test_import_types_documented(self, readme_content): | ||
| 344 | """Test import types are documented.""" | ||
| 345 | assert "rootfs_dir" in readme_content | ||
| 346 | assert "qcow2" in readme_content | ||
| 347 | |||
| 348 | def test_kernel_modes_documented(self, readme_content): | ||
| 349 | """Test kernel modes are documented.""" | ||
| 350 | assert "none" in readme_content | ||
| 351 | assert "Shared host kernel" in readme_content or "shared" in readme_content.lower() | ||
| 352 | |||
| 353 | def test_alpine_example(self, readme_content): | ||
| 354 | """Test Alpine example is in README.""" | ||
| 355 | assert "alpine" in readme_content.lower() | ||
| 356 | assert "XEN_GUEST_SOURCE_TYPE" in readme_content | ||
| 357 | |||
| 358 | def test_custom_handler_docs(self, readme_content): | ||
| 359 | """Test custom handler instructions.""" | ||
| 360 | assert "xen_guest_import_" in readme_content | ||
| 361 | assert "XEN_GUEST_IMPORT_DEPENDS_" in readme_content | ||
