Repository: osbuild/osbuild
Pull request #1567 (2024-02-02 04:21:17)

stages(copy): allow copying from a mount or the tree
It seems like an artifical limitation to prevent copying from one location in the tree to another. It just so happens we need this functionality when building CoreOS images because we want to take a file embedded in the OSTree at a location and copy it to another location in the tree. The particular example here is we want to copy /usr/share/coreos-assembler/platforms.json -> /boot/coreos/platforms.json See https://github.com/coreos/coreos-assembler/pull/3709

Allowing to copy from/to the tree we can now do something like:

```
- type: org.osbuild.copy
  options:
    paths:
      - from: tree:///usr/share/coreos-assembler/platforms.json
        to: tree:///boot/coreos/platforms.json
  mounts:
    - name: ostree.deployment
      type: org.osbuild.ostree.deployment
      options:
        deployment:
          ref: ostree/1/1/0
          osname:
            fedora-coreos
```

dustymabe: If this PR doesn't get accepted I'll need help to figure out how to achieve the goal here. I could try copying using a previous stage as input, but there's really not any way to do that with an OSTree tree because we won't know the deployment path so we can't know the absolute path to the file that I want to copy.

With this (the example I showed above) I'm able to use the special `org.osbuild.ostree.deployment` mount to mount the deployment path on the tree so I don't need to know anything and I can happily just `cp /usr/share/coreos-assembler/platforms.json /boot/coreos/platforms.json`. 
thozza: So, I think that we already do something similar in the `osbuild/images`, see https://github.com/osbuild/images/blob/a7122313c58186a2ba1fc7b2cc6483a0c7c8e66d/pkg/manifest/raw_ostree.go#L72-L106
supakeen: I don't particularly see this as a thing we shouldn't be allowing, I think the stage was written narrowly to be expanded later (like: now).
dustymabe: > Thanks, if you want to add it to this PR you can add the `mount://` as well; if not we can add it when its needed it in another PR.

added `mount://`! 

Patch:
diff --git a/stages/org.osbuild.container-deploy b/stages/org.osbuild.container-deploy
index 43c2076b..7f419da4 100755
--- a/stages/org.osbuild.container-deploy
+++ b/stages/org.osbuild.container-deploy
@@ -47,12 +47,16 @@ def mount_container(image_tag):
     try:
         result = subprocess.run(
             ["podman", "image", "mount", image_tag],
-            capture_output=True,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
             encoding="utf-8",
-            check=True,
+            check=False,
         )
+        if result.returncode != 0:
+            code = result.returncode
+            msg = result.stderr.strip()
+            raise RuntimeError(f"Failed to mount image ({code}): {msg}")
         yield result.stdout.strip()
-
     finally:
         subprocess.run(
             ["podman", "image", "umount", image_tag],
diff --git a/stages/org.osbuild.copy b/stages/org.osbuild.copy
index 3c137af5..5bc9067f 100755
--- a/stages/org.osbuild.copy
+++ b/stages/org.osbuild.copy
@@ -45,9 +45,23 @@ SCHEMA_2 = r"""
         "required": ["from", "to"],
         "properties": {
           "from": {
-            "type": "string",
-            "description": "The source",
-            "pattern": "^input:\/\/[^\/]+\/"
+            "oneOf": [
+              {
+                "type": "string",
+                "description": "The source, if an input",
+                "pattern": "^input:\/\/[^\/]+\/"
+              },
+              {
+                "type": "string",
+                "description": "The source, if a mount",
+                "pattern": "^mount:\/\/[^\/]+\/"
+              },
+              {
+                "type": "string",
+                "description": "The source, if the tree",
+                "pattern": "^tree:\/\/\/"
+              }
+            ]
           },
           "to": {
             "oneOf": [
@@ -58,7 +72,7 @@ SCHEMA_2 = r"""
               },
               {
                 "type": "string",
-                "description": "The destination, if a tree",
+                "description": "The destination, if the tree",
                 "pattern": "^tree:\/\/\/"
               }
             ]
diff --git a/stages/org.osbuild.zipl.inst b/stages/org.osbuild.zipl.inst
index a9bea045..caca9f82 100755
--- a/stages/org.osbuild.zipl.inst
+++ b/stages/org.osbuild.zipl.inst
@@ -28,6 +28,14 @@ SCHEMA_2 = r"""
     "location": {
       "type": "number"
     },
+    "kernel_opts_append": {
+      "description": "Additional kernel options to add to the discovered options",
+      "type": "array",
+      "items": {
+        "description": "A single kernel command line option",
+        "type": "string"
+      }
+    },
     "blocksize": {
       "type": "number",
       "default": 512
@@ -85,6 +93,7 @@ def find_kernel(root, kernel: str):
 def main(paths, devices, options):
     kver = options["kernel"]
     location = options["location"]
+    append_kopts = options.get("kernel_opts_append", [])
     blocksize = options.get("blocksize", 512)
 
     root = paths["mounts"]
@@ -100,7 +109,7 @@ def main(paths, devices, options):
                     "--target", f"{root}/boot",
                     "--image", kernel,
                     "--ramdisk", initrd,
-                    "--parameters", kopts,
+                    "--parameters", f"{kopts} {' '.join(append_kopts)}" if append_kopts else kopts,
                     "--targetbase", device,
                     "--targettype", "SCSI",
                     "--targetblocksize", str(blocksize),
diff --git a/tools/osbuild-depsolve-dnf b/tools/osbuild-depsolve-dnf
index a54473b8..fdc34624 100755
--- a/tools/osbuild-depsolve-dnf
+++ b/tools/osbuild-depsolve-dnf
@@ -121,6 +121,11 @@ class Solver():
         # we set the expiration to a short time period, rather than 0.
         repo.metadata_expire = desc.get("metadata_expire", "20s")
 
+        # This option if True disables modularization filtering. Effectively
+        # disabling modularity for given repository.
+        if "module_hotfixes" in desc:
+            repo.module_hotfixes = desc["module_hotfixes"]
+
         return repo
 
     @staticmethod
@@ -201,11 +206,13 @@ class Solver():
     def depsolve(self, transactions):
         last_transaction = []
 
-        for transaction in transactions:
+        for idx, transaction in enumerate(transactions):
             self.base.reset(goal=True)
             self.base.sack.reset_excludes()
 
-            self.base.conf.install_weak_deps = transaction.get("install_weak_deps", False)
+            # don't install weak-deps for transactions after the 1st transaction
+            if idx > 0:
+                self.base.conf.install_weak_deps = False
 
             # set the packages from the last transaction as installed
             for installed_pkg in last_transaction:


Test patch:
diff --git a/stages/test/test_zipl_inst.py b/stages/test/test_zipl_inst.py
new file mode 100644
index 00000000..8e867fbb
--- /dev/null
+++ b/stages/test/test_zipl_inst.py
@@ -0,0 +1,82 @@
+#!/usr/bin/python3
+
+import os
+import textwrap
+from unittest.mock import patch
+
+from osbuild.testutil import make_fake_tree
+
+STAGE_NAME = "org.osbuild.zipl.inst"
+
+
+@patch("subprocess.run")
+def test_zipl_inst_default(mocked_run, tmp_path, stage_module):
+    make_fake_tree(tmp_path, {
+        "/boot/loader/entries/ostree-1-fedora-coreos.conf": textwrap.dedent("""\
+        title Fedora CoreOS 39.20240206.dev.0 (ostree:0)
+        version 1
+        options ignition.platform.id=qemu ostree=/ostree/boot.1/fedora-coreos/abcd/0
+        linux /vmlinuz-6.6.14-200.fc39.s390x
+        initrd /initramfs-6.6.14-200.fc39.s390x.img
+        """),
+        "/vmlinuz-6.6.14-200.fc39.s390x": "fake-vmlinuz",
+        "/initramfs-6.6.14-200.fc39.s390x.img": "fake-initramfs.img",
+    })
+    fake_paths = {
+        "mounts": os.fspath(tmp_path),
+        "devices": "/run/osbuild/dev",
+    }
+    fake_devices = {
+        "disk": {
+            "path": "some-device-path",
+        }
+    }
+    fake_options = {
+        "kernel": "1",
+        "location": "some-location",
+    }
+
+    stage_module.main(fake_paths, fake_devices, fake_options)
+    assert len(mocked_run.call_args_list) == 1
+    args, kwargs = mocked_run.call_args_list[0]
+    assert len(args) == 1
+    run_argv = args[0]
+    assert "ignition.platform.id=qemu ostree=/ostree/boot.1/fedora-coreos/abcd/0" in run_argv
+    assert kwargs["check"]
+
+
+@patch("subprocess.run")
+def test_zipl_inst_kernel_opts_append(mocked_run, tmp_path, stage_module):
+    make_fake_tree(tmp_path, {
+        "/boot/loader/entries/00_linux.conf": textwrap.dedent("""\
+        title fake-linux
+        linux /vmlinuz
+        initrd /initrd.img
+        options root=/dev/sda1 ro
+        version 6.8
+        """),
+        "/vmlinuz": "fake-vmlinuz",
+        "/initrd.img": "fake-initrd.img",
+    })
+    fake_paths = {
+        "mounts": os.fspath(tmp_path),
+        "devices": "/run/osbuild/dev",
+    }
+    fake_devices = {
+        "disk": {
+            "path": "some-device-path",
+        }
+    }
+    fake_options = {
+        "kernel": "6.8",
+        "location": "some-location",
+        "kernel_opts_append": ["some=more", "opts"],
+    }
+
+    stage_module.main(fake_paths, fake_devices, fake_options)
+    assert len(mocked_run.call_args_list) == 1
+    args, kwargs = mocked_run.call_args_list[0]
+    assert len(args) == 1
+    run_argv = args[0]
+    assert "root=/dev/sda1 ro some=more opts" in run_argv
+    assert kwargs["check"]