Repository: osbuild/osbuild (python)

Issue:
Building image on a ppc64le system fails
Running `image-builder build qcow2 --distro fedora-41` on a ppc64le system fails with:

```
Finished module org.osbuild.mkfs.ext4
Starting module org.osbuild.mkfs.ext4
device/device (org.osbuild.loopback): loop0 acquired (locked: True)
Failed to open file "/sys/fs/selinux/checkreqprot": Read-only file system
mke2fs 1.47.1 (20-May-2024)
Discarding device blocks:
0/1181440


done
Creating filesystem with 1181440 4k blocks and 295408 inodes
Filesystem UUID: 72d289ef-6d6b-4602-a310-2ee5f1267bd7
Superblock backups stored on blocks:
        32768, 98304, 163840, 229376, 294912, 819200, 884736

Allocating group tables: done
Writing inode tables: done
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information:  0/37

done
Finished module org.osbuild.mkfs.ext4
Starting module org.osbuild.copy
device/- (org.osbuild.loopback): loop0 acquired (locked: False)
device/boot (org.osbuild.loopback): loop1 acquired (locked: False)
mount/- (org.osbuild.ext4): mounting /dev/loop0 -> /var/lib/copr-rpmbuild/tmp/buildroot-tmp-l_e_obl3/mounts/
mount/boot (org.osbuild.ext4): mounting /dev/loop1 -> /var/lib/copr-rpmbuild/tmp/buildroot-tmp-l_e_obl3/mounts/boot
Failed to open file "/sys/fs/selinux/checkreqprot": Read-only file system
copying '/run/osbuild/inputs/root-tree/.' -> '/run/osbuild/mounts/.'
mount/boot (org.osbuild.ext4): umount: /var/lib/copr-rpmbuild/tmp/buildroot-tmp-l_e_obl3/mounts/boot unmounted
mount/- (org.osbuild.ext4): umount: /var/lib/copr-rpmbuild/tmp/buildroot-tmp-l_e_obl3/mounts/ unmounted
Finished module org.osbuild.copy
Starting module org.osbuild.grub2.inst
Failed to open file "/sys/fs/selinux/checkreqprot": Read-only file system
prefix: (,msdos2)/grub2
grub2-mkimage: info: the total module size is 0x643c.
grub2-mkimage: info: reading /usr/lib/grub/powerpc-ieee1275/kernel.img.
grub2-mkimage: error: invalid ELF header.
Traceback (most recent call last):
  File "/run/osbuild/bin/org.osbuild.grub2.inst", line 192, in <module>
    r = main(args["tree"], args["options"])
  File "/run/osbuild/bin/org.osbuild.grub2.inst", line 178, in main
    core_path = core_mkimage(platform, prefix, options["core"])
  File "/run/osbuild/bin/org.osbuild.grub2.inst", line 114, in core_mkimage
    subprocess.run([options.get("binary", "grub2-mkimage"),
    ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                    "--verbose",
                    ^^^^^^^^^^^^
    ...<5 lines>...
                   modules,
                   ^^^^^^^^
                   check=True)
                   ^^^^^^^^^^^
  File "/usr/lib64/python3.13/subprocess.py", line 579, in run
    raise CalledProcessError(retcode, process.args,
                             output=stdout, stderr=stderr)
subprocess.CalledProcessError: Command '['grub2-mkimage', '--verbose', '--directory', '/usr/lib/grub/powerpc-ieee1275', '--prefix', '(,msdos2)/grub2', '--format', 'i386-pc', '--compression', 'auto', '--output', '/var/tmp/grub2-core.img', 'part_msdos', 'ext2']' returned non-zero exit status 1.
Finished module org.osbuild.grub2.inst
Finished pipeline image
manifest - failed
Output:
Failed
```

Pull request description:
stages/grub2.inst: fix format for non-x86_64 image
When writing the grub2 core image, the format should be set based on the platform.  This was accidentally changed to be `i386-pc` for all platforms except when building ISOs.

Set the format to the platform and only change it to `i386-pc-eltorito` when building ISOs.

Fixes #2037



Patch:
diff --git a/stages/org.osbuild.grub2.inst b/stages/org.osbuild.grub2.inst
index ecc9d2a8..4f4bf6ef 100755
--- a/stages/org.osbuild.grub2.inst
+++ b/stages/org.osbuild.grub2.inst
@@ -4,6 +4,7 @@ import shutil
 import struct
 import subprocess
 import sys
+import tempfile
 from typing import BinaryIO, Dict
 
 import osbuild.api
@@ -70,12 +71,11 @@ def write_core_image(core_f, image_f, location, sector_size):
     shutil.copyfileobj(core_f, image_f)
 
 
-def core_mkimage(platform: str, prefix: str, options: Dict):
+def core_mkimage(platform: str, prefix: str, options: Dict, core_dir: str):
     pt_label = options["partlabel"]
     fs_type = options["filesystem"]
 
-    os.makedirs("/var/tmp/", exist_ok=True)
-    core_path = "/var/tmp/grub2-core.img"
+    core_path = os.path.join(core_dir, "grub2-core.img")
 
     # Create the level-2 & 3 stages of the bootloader, aka the core
     # it consists of the kernel plus the core modules required to
@@ -86,12 +86,11 @@ def core_mkimage(platform: str, prefix: str, options: Dict):
     # read the partition and its filesystem containing said modules
     # and the grub configuration [NB: efi systems work differently]
 
+    gformat = platform
     if platform == "i386-pc":
         modules = ["biosdisk"]
-        gformat = "i386-pc"
     else:
         modules = []
-        gformat = "i386-pc"
 
     if pt_label in ["dos", "mbr"]:
         modules += ["part_msdos"]
@@ -175,14 +174,16 @@ def main(tree, options):
     else:
         prefix = options["prefix"]["path"]
     print(f"prefix: {prefix}")
-    core_path = core_mkimage(platform, prefix, options["core"])
 
-    location = options.get("location")
-    if location:
-        patch_core(location, core_path, image, sector_size, platform)
-    else:
-        # If location isn't set, use the image file as-is instead of with the MBR
-        shutil.copyfile(core_path, image)
+    with tempfile.TemporaryDirectory() as core_tmpdir:
+        core_path = core_mkimage(platform, prefix, options["core"], core_tmpdir)
+
+        location = options.get("location")
+        if location:
+            patch_core(location, core_path, image, sector_size, platform)
+        else:
+            # If location isn't set, use the image file as-is instead of with the MBR
+            shutil.copyfile(core_path, image)
 
     return 0
 


Test patch:
diff --git a/stages/test/test_grub2_inst.py b/stages/test/test_grub2_inst.py
index 5677112c..a49229e1 100644
--- a/stages/test/test_grub2_inst.py
+++ b/stages/test/test_grub2_inst.py
@@ -1,6 +1,7 @@
 #!/usr/bin/python3
 import os
 import subprocess
+from unittest.mock import call, patch
 
 import pytest
 
@@ -68,3 +69,50 @@ def test_grub2_iso9660(tmp_path, stage_module):
     }
     stage_module.main(treedir, options)
     assert os.path.exists(treedir / "eltorito.img")
+
+
+@patch("subprocess.run")
+def test_grub2_partition_mocked(mocked_run, tmp_path, stage_module):
+    options = {
+        "filename": "disk.img",
+        "platform": "some-platform",
+        "location": 0,
+        "core": {
+            "type": "mkimage",
+            "partlabel": "gpt",
+            "filesystem": "ext4",
+        },
+        "prefix": {
+            "type": "partition",
+            "partlabel": "gpt",
+            "number": 0,
+            "path": "/boot/",
+        },
+    }
+
+    output_path = ""
+
+    def mock_side_effect(args, **_):
+        """
+        Intercept the args to subprocess.run() to leak the random output path and create the file that's expected by the
+        shutil.copy() in the stage's main().
+        """
+        nonlocal output_path
+        output_path = args[args.index("--output") + 1]
+        with open(output_path, "wb") as fake_image_file:
+            fake_image_file.write(b"I am a grub core image")
+
+    mocked_run.side_effect = mock_side_effect
+
+    stage_module.main(tmp_path, options)
+
+    assert os.path.basename(output_path) == "grub2-core.img"
+    assert mocked_run.call_args_list == [
+        call(["grub2-mkimage", "--verbose",
+              "--directory", "/usr/lib/grub/some-platform",
+              "--prefix", "(,gpt1)/boot/",
+              "--format", "some-platform",
+              "--compression", "auto",
+              "--output", output_path,
+              "part_gpt", "ext2"], check=True),
+    ]