Repository: osbuild/osbuild
Pull request #2131 (2025-06-27 12:34:55)

tests/CI: Upgrade rhel 9.5 GA to 9.6 GA and add 10.0 GA
Upgrade CI to 9.6 GA and 10.0 GA (first GA runners for rhel 10) and remove 9.6 and 10.0 nightly runners from the CI.


Patch:
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4132836f..183d102c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -45,15 +45,13 @@ RPM:
           - aws/centos-stream-10-aarch64
           - aws/rhel-8.10-ga-x86_64
           - aws/rhel-8.10-ga-aarch64
-          - aws/rhel-9.5-ga-x86_64
-          - aws/rhel-9.5-ga-aarch64
+          - aws/rhel-9.6-ga-x86_64
+          - aws/rhel-9.6-ga-aarch64
+          - aws/rhel-10.0-ga-x86_64
+          - aws/rhel-10.0-ga-aarch64
       - RUNNER:
-          - aws/rhel-9.6-nightly-x86_64
-          - aws/rhel-9.6-nightly-aarch64
           - aws/rhel-9.7-nightly-x86_64
           - aws/rhel-9.7-nightly-aarch64
-          - aws/rhel-10.0-nightly-x86_64
-          - aws/rhel-10.0-nightly-aarch64
           - aws/rhel-10.1-nightly-x86_64
           - aws/rhel-10.1-nightly-aarch64
         INTERNAL_NETWORK: "true"
@@ -142,14 +140,12 @@ Manifests:
         CHUNK: [1, 2, 3, 4, 5]
         CHUNKS_COUNT: 5
       - RUNNER:
-          - aws/rhel-9.5-ga-x86_64
-          - aws/rhel-9.5-ga-aarch64
-          - aws/rhel-9.6-nightly-x86_64
-          - aws/rhel-9.6-nightly-aarch64
+          - aws/rhel-9.6-ga-x86_64
+          - aws/rhel-9.6-ga-aarch64
           - aws/rhel-9.7-nightly-x86_64
           - aws/rhel-9.7-nightly-aarch64
-          - aws/rhel-10.0-nightly-x86_64
-          - aws/rhel-10.0-nightly-aarch64
+          - aws/rhel-10.0-ga-x86_64
+          - aws/rhel-10.0-ga-aarch64
           - aws/rhel-10.1-nightly-x86_64
           - aws/rhel-10.1-nightly-aarch64
         INTERNAL_NETWORK: "true"
diff --git a/Schutzfile b/Schutzfile
index 370aa033..72999b32 100644
--- a/Schutzfile
+++ b/Schutzfile
@@ -150,47 +150,6 @@
       }
     ]
   },
-  "rhel-9.6": {
-    "repos": [
-      {
-        "file": "/etc/yum.repos.d/rhel9internal.repo",
-        "x86_64": [
-          {
-            "title": "RHEL-9-RPMREPO-NIGHTLY-BaseOS",
-            "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-baseos-n9.6-20250626"
-          },
-          {
-            "title": "RHEL-9-RPMREPO-NIGHTLY-AppStream",
-            "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-appstream-n9.6-20250626"
-          },
-          {
-            "title": "RHEL-9-RPMREPO-NIGHTLY-CRB",
-            "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-crb-n9.6-20250626"
-          }
-        ],
-        "aarch64": [
-          {
-            "title": "RHEL-9-RPMREPO-NIGHTLY-BaseOS",
-            "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-baseos-n9.6-20250626"
-          },
-          {
-            "title": "RHEL-9-RPMREPO-NIGHTLY-AppStream",
-            "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-appstream-n9.6-20250626"
-          },
-          {
-            "title": "RHEL-9-RPMREPO-NIGHTLY-CRB",
-            "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-crb-n9.6-20250626"
-          }
-        ]
-      }
-    ]
-  },
   "rhel-9.7": {
     "repos": [
       {
@@ -232,47 +191,6 @@
       }
     ]
   },
-  "rhel-10.0": {
-    "repos": [
-      {
-        "file": "/etc/yum.repos.d/rhel10internal.repo",
-        "x86_64": [
-          {
-            "title": "RHEL-10-RPMREPO-NIGHTLY-BaseOS",
-            "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-x86_64-baseos-n10.0-20250626"
-          },
-          {
-            "title": "RHEL-10-RPMREPO-NIGHTLY-AppStream",
-            "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-x86_64-appstream-n10.0-20250626"
-          },
-          {
-            "title": "RHEL-10-RPMREPO-NIGHTLY-CRB",
-            "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-x86_64-crb-n10.0-20250626"
-          }
-        ],
-        "aarch64": [
-          {
-            "title": "RHEL-10-RPMREPO-NIGHTLY-BaseOS",
-            "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-aarch64-baseos-n10.0-20250626"
-          },
-          {
-            "title": "RHEL-10-RPMREPO-NIGHTLY-AppStream",
-            "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-aarch64-appstream-n10.0-20250626"
-          },
-          {
-            "title": "RHEL-10-RPMREPO-NIGHTLY-CRB",
-            "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-aarch64-crb-n10.0-20250626"
-          }
-        ]
-      }
-    ]
-  },
   "rhel-10.1": {
     "repos": [
       {
@@ -314,4 +232,4 @@
       }
     ]
   }
-}
\ No newline at end of file
+}
diff --git a/schutzbot/mockbuild.sh b/schutzbot/mockbuild.sh
index 2c7f83e9..d8ccd415 100755
--- a/schutzbot/mockbuild.sh
+++ b/schutzbot/mockbuild.sh
@@ -9,11 +9,7 @@ function greenprint {
 # function to override template respositores with system repositories which contain rpmrepos snapshots
 function template_override {
     sudo dnf -y install jq
-    if sudo subscription-manager status; then
-        greenprint "📋 Running on subscribed RHEL machine, no mock template override done."
-        return 0
-    fi
-
+    
     # TODO: remove this, once mock-core-configs ships a template for RHEL-10
     # Use RHEL-9 template as the baseline for now.
     if [[ "$ID" == rhel && ${VERSION_ID%.*} == 10 ]]; then
@@ -23,10 +19,17 @@ function template_override {
         sudo sed -i "s/config_opts\['releasever'\] = '9'/config_opts\['releasever'\] = '10'/" /etc/mock/templates/"$TEMPLATE"
         # disable bootstrap image for el10, as there is none yet
         sudo sed -i "s/config_opts\['bootstrap_image_ready'\] = True/config_opts\['bootstrap_image_ready'\] = False/" /etc/mock/templates/"$TEMPLATE"
+        # update hardcoded rhel9 paths to rhel10 in repository URLs
+        sudo sed -i "s/rhel9/rhel10/g" /etc/mock/templates/"$TEMPLATE"
 
         sudo cp /etc/mock/rhel-{9,10}-"$(uname -m)".cfg
         sudo sed -i "s/rhel-9/rhel-10/" "/etc/mock/rhel-10-$(uname -m).cfg"
     fi
+    
+    if sudo subscription-manager status; then
+        greenprint "📋 Running on subscribed RHEL machine, no mock template override done."
+        return 0
+    fi
 
     if [[ "$ID" == rhel ]]; then
         TEMPLATE=${ID}-${VERSION_ID%.*}.tpl
diff --git a/schutzbot/terraform b/schutzbot/terraform
index 9a64fd4c..a3ddc921 100644
--- a/schutzbot/terraform
+++ b/schutzbot/terraform
@@ -1,1 +1,1 @@
-7b4735d287dd0950e0a6f47dde65b62b0f239da1
+cf0a810fd3b75fa27139746c4dfe72222e13dcba
diff --git a/stages/org.osbuild.ovf b/stages/org.osbuild.ovf
index cfe1cbf5..e0bcb830 100755
--- a/stages/org.osbuild.ovf
+++ b/stages/org.osbuild.ovf
@@ -1,20 +1,22 @@
 #!/usr/bin/python3
 import json
 import os
+import random
 import subprocess
 import sys
+import uuid
 
 import osbuild.api
 from osbuild.util import checksum
 
 OVF_TEMPLATE = """<?xml version="1.0"?>
-<Envelope xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:cim="http://schemas.dmtf.org/wbem/wscim/1/common" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vmw="http://www.vmware.com/schema/ovf" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+<Envelope xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:cim="http://schemas.dmtf.org/wbem/wscim/1/common" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vmw="http://www.vmware.com/schema/ovf" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:vbox="http://www.virtualbox.org/ovf/machine">
   <References>
     <File ovf:href="{image_name}" ovf:id="file1" ovf:size="{vmdk_size}"/>
   </References>
   <DiskSection>
     <Info>Virtual disk information</Info>
-    <Disk ovf:capacity="{vmdk_capacity}" ovf:capacityAllocationUnits="byte" ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized" ovf:populatedSize="{vmdk_size}"/>
+    <Disk ovf:capacity="{vmdk_capacity}" ovf:capacityAllocationUnits="byte" ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized" ovf:populatedSize="{vmdk_size}" vbox:uuid="{vbox_disk_uuid}"/>
   </DiskSection>
   <NetworkSection>
     <Info>The list of logical networks</Info>
@@ -22,7 +24,7 @@ OVF_TEMPLATE = """<?xml version="1.0"?>
   <VirtualSystem ovf:id="image">
     <Info>A virtual machine</Info>
     <Name>VM</Name>
-    <OperatingSystemSection ovf:id="100" vmw:osType="other26xLinux64Guest">
+    <OperatingSystemSection ovf:id="100" vmw:osType="{vmware_os_type}">
       <Info>The kind of installed guest operating system</Info>
     </OperatingSystemSection>
     <VirtualHardwareSection ovf:transport="com.vmware.guestInfo">
@@ -84,41 +86,119 @@ OVF_TEMPLATE = """<?xml version="1.0"?>
       <vmw:Config ovf:required="false" vmw:key="bootOptions.efiSecureBootEnabled" vmw:value="false"/>
       <vmw:Config ovf:required="false" vmw:key="firmware" vmw:value="efi"/>
     </VirtualHardwareSection>
+    <vbox:Machine ovf:required="false" version="1.16-linux" uuid="{vbox_machine_uuid}" name="packer-virtualbox-UNIX" OSType="{vbox_os_type}" snapshotFolder="Snapshots">
+      <ovf:Info>Complete VirtualBox machine configuration in VirtualBox format</ovf:Info>
+      <Hardware>
+        <CPU count="2">
+          <PAE enabled="true"/>
+          <LongMode enabled="true"/>
+          <X2APIC enabled="true"/>
+          <HardwareVirtExLargePages enabled="false"/>
+        </CPU>
+        <Memory RAMSize="1024"/>
+        <Boot>
+          <Order position="1" device="HardDisk"/>
+          <Order position="2" device="DVD"/>
+          <Order position="3" device="None"/>
+          <Order position="4" device="None"/>
+        </Boot>
+        <BIOS>
+          <IOAPIC enabled="true"/>
+          <SmbiosUuidLittleEndian enabled="true"/>
+        </BIOS>
+        <Network>
+          <Adapter slot="0" enabled="true" MACAddress="{vbox_mac_address}" type="82540EM">
+            <NAT/>
+          </Adapter>
+        </Network>
+        <AudioAdapter driver="OSS" enabledIn="false" enabledOut="false"/>
+        <Clipboard/>
+      </Hardware>
+      <StorageControllers>
+        <StorageController name="IDE Controller" type="PIIX4" PortCount="2" useHostIOCache="true" Bootable="true">
+          <AttachedDevice type="HardDisk" hotpluggable="false" port="0" device="0">
+            <Image uuid="{vbox_disk_uuid}"/>
+          </AttachedDevice>
+        </StorageController>
+      </StorageControllers>
+    </vbox:Machine>
   </VirtualSystem>
 </Envelope>
 """
 
 
+def vbox_mac_address(options):
+    # If we have an explicit mac address we use that.
+    mac_address = options.get("virtualbox", {}).get("mac_address")
+
+    if mac_address:
+        return mac_address
+
+    # Otherwise we generate one according to how VirtualBox usually does this:
+    # https://github.com/mirror/vbox/blob/b9657cd5351cf17432b664009cc25bb480dc64c1/src/VBox/Main/src-server/HostImpl.cpp#L3267
+    # VirtualBox-6.1.12 src/VBox/NetworkServices/Dhcpd/Config.cpp line 276
+    mac_address = "080027"
+
+    for _ in range(0, 3):
+        mac_address += "".join(random.sample("0123456789abcdef", 2))
+
+    return mac_address
+
+
 def virtual_size(vmdk):
     cmd = ["qemu-img", "info", "--output=json", vmdk]
-    res = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, encoding="utf8")
+    res = subprocess.run(
+        cmd, check=True, stdout=subprocess.PIPE, encoding="utf8"
+    )
+
     if res.returncode != 0:
         raise RuntimeError("Unable to determine vmdk size")
+
     return json.loads(res.stdout)["virtual-size"]
 
 
-def write_template(vmdk):
+def write_template(vmdk, options):
     dirname, basename = os.path.split(vmdk)
-    ovf_data = OVF_TEMPLATE.format(vmdk_size=os.stat(vmdk).st_size,
-                                   vmdk_capacity=virtual_size(vmdk), image_name=basename)
+
+    ovf_data = OVF_TEMPLATE.format(
+        vmdk_size=os.stat(vmdk).st_size,
+        vmdk_capacity=virtual_size(vmdk),
+        vmware_os_type=options.get("vmware", {}).get("os_type", "other26xLinux64Guest"),
+        vbox_machine_uuid=str(uuid.uuid4()),
+        vbox_disk_uuid=str(uuid.uuid4()),
+        vbox_os_type=options.get("virtualbox", {}).get("os_type", "OtherLinux_64"),
+        vbox_mac_address=vbox_mac_address(options),
+        image_name=basename,
+    )
+
     ovf = f"{os.path.join(dirname, os.path.splitext(basename)[0])}.ovf"
+
     with open(ovf, "w", encoding="utf8") as f:
         f.write(ovf_data)
+
     return ovf
 
 
 def write_manifest(vmdk, ovf):
     dirname, basename = os.path.split(vmdk)
+
     mf = f"{os.path.join(dirname, os.path.splitext(basename)[0])}.mf"
+
     with open(mf, "w", encoding="utf8") as f:
-        f.write(f"SHA256({os.path.basename(ovf)})= {checksum.hexdigest_file(ovf, 'sha256')}\n")
-        f.write(f"SHA256({basename})= {checksum.hexdigest_file(vmdk, 'sha256')}\n")
+        f.write(
+            f"SHA256({os.path.basename(ovf)})= {checksum.hexdigest_file(ovf, 'sha256')}\n"
+        )
+
+        f.write(
+            f"SHA256({basename})= {checksum.hexdigest_file(vmdk, 'sha256')}\n"
+        )
 
 
 def main(options, tree):
     vmdk = os.path.join(tree, options["vmdk"])
-    ovf = write_template(vmdk)
+    ovf = write_template(vmdk, options)
     write_manifest(vmdk, ovf)
+
     return 0
 
 
diff --git a/stages/org.osbuild.ovf.meta.json b/stages/org.osbuild.ovf.meta.json
index a9eee1cf..1f0fe8ce 100644
--- a/stages/org.osbuild.ovf.meta.json
+++ b/stages/org.osbuild.ovf.meta.json
@@ -18,6 +18,32 @@
           "description": "The vmdk image filename present in the root of the tree",
           "type": "string",
           "pattern": "[a-zA-Z0-9+_.-]+.vmdk"
+        },
+        "vmware": {
+          "description": "Settings for VMWare specific parts of the OVF file.",
+          "type": "object",
+          "additionalProperties": false,
+          "properties": {
+            "os_type": {
+              "type": "string",
+              "default": "other26xLinux64Guest"
+            }
+          }
+        },
+        "virtualbox": {
+          "description": "Settings for VirtualBox specific parts of the OVF file.",
+          "type": "object",
+          "additionalProperties": false,
+          "properties": {
+            "os_type": {
+              "type": "string",
+              "default": "OtherLinux_64"
+            },
+            "mac_address": {
+              "type": "string",
+              "pattern": "^[a-fA-F0-9]{12}$"
+            }
+          }
         }
       }
     }


Test patch:
diff --git a/stages/test/test_ovf.py b/stages/test/test_ovf.py
index 72d978e4..c8b41f16 100644
--- a/stages/test/test_ovf.py
+++ b/stages/test/test_ovf.py
@@ -13,8 +13,13 @@ STAGE_NAME = "org.osbuild.ovf"
     ({}, "'vmdk' is a required property"),
     ({"vmdk": 123}, "123 is not of type 'string'"),
     ({"vmdk": "imagename"}, "'imagename' does not match '[a-zA-Z0-9+_.-]+.vmdk'"),
+    ({"vmdk": "imagename.vmdk", "vmware": {"os_type": 1}}, "is not of type 'string'"),
+    ({"vmdk": "imagename.vmdk", "virtualbox": {"os_type": 1}}, "is not of type 'string'"),
+    ({"vmdk": "imagename.vmdk", "virtualbox": {"mac_address": 1}}, "is not of type 'string'"),
+    ({"vmdk": "imagename.vmdk", "virtualbox": {"mac_address": "1"}}, "'1' does not match '^[a-fA-F0-9]{12}$'"),
     # Good API parameters
     ({"vmdk": "imagename.vmdk"}, ""),
+    ({"vmdk": "imagename.vmdk", "vmware": {"os_type": "OtherGuest"}, "virtualbox": {"os_type": "OtherGuest"}}, ""),
 ])
 # This test validates only API calls using correct and incorrect queries
 def test_schema_validation_ovf(stage_schema, test_data, expected_err):
@@ -39,14 +44,39 @@ def test_schema_validation_ovf(stage_schema, test_data, expected_err):
 
 
 @pytest.mark.skipif(not testutil.has_executable("qemu-img"), reason="need qemu-img")
-def test_ovf_default_template(tmp_path, stage_module):
+@pytest.mark.parametrize("test_opts, expected_substrings", [
+    # Replacements
+    ({"vmware": {"os_type": "my-vmware-os-type"}}, ["my-vmware-os-type"],),
+    ({"virtualbox": {"os_type": "my-vbox-os-type"}}, ["my-vbox-os-type"],),
+    ({"virtualbox": {"mac_address": "000000000000"}}, ["000000000000"],),
+])
+def test_ovf_default_template(tmp_path, stage_module, test_opts, expected_substrings):
     faked_vmdk_path = tmp_path / "some-image.vmdk"
     faked_vmdk_path.write_bytes(b"1234")
 
     opts = {
         "vmdk": faked_vmdk_path,
     }
+    opts.update(test_opts)
+
     stage_module.main(opts, tmp_path)
 
     expected_template_path = tmp_path / "some-image.ovf"
-    assert "other26xLinux64Guest" in expected_template_path.read_text()
+
+    for substring in expected_substrings:
+        assert substring in expected_template_path.read_text()
+
+
+def test_ovf_vbox_mac_address_differs(stage_module):
+    macs = [
+        stage_module.vbox_mac_address({}),
+        stage_module.vbox_mac_address({}),
+    ]
+
+    assert macs[0] != macs[1]
+    assert all(m.startswith("080027") for m in macs)
+    assert all(len(m) == 12 for m in macs)
+
+
+def test_ovf_vbox_mac_address_options(stage_module):
+    assert stage_module.vbox_mac_address({"virtualbox": {"mac_address": "000000000000"}}) == "000000000000"