Repository: osbuild/osbuild
Pull request #1501 (2023-12-13 14:28:53)

mounts: support mounting partitions
This allows us to map in a whole disk as a loopback device with partition
scanning rather than slicing up the disk and creating several loopback
devices.

This depends on https://github.com/osbuild/osbuild/pull/1493 merging first.

Fixes https://github.com/osbuild/osbuild/issues/1495

dustymabe: It seems like changing the function signature here means I will need to update https://github.com/osbuild/manifest-db/blob/3de3aff3d99baec9e987aa8f71b66c15120149da/tools/image-info#L2680-L2686 I think. Shutzbot CI won't pass here until that happens but it's kind of a chicken/egg problem I think.
dustymabe: > It seems like changing the function signature here means I will need to update https://github.com/osbuild/manifest-db/blob/3de3aff3d99baec9e987aa8f71b66c15120149da/tools/image-info#L2680-L2686 I think. Shutzbot CI won't pass here until that happens but it's kind of a chicken/egg problem I think.

I opened https://github.com/osbuild/manifest-db/pull/101 to address this.
dustymabe: so... I think we are the point where all tests pass except the ones that require the https://github.com/osbuild/manifest-db/pull/101 change. 
dustymabe: ok https://github.com/osbuild/osbuild/pull/1493 merged. I'll rebase this on top of latest `main` and it should be ready to go. 
ondrejbudai: @mvo5 May I ask for a review here? I think you are now fairly familiar with the loop bits in osbuild. :)
dustymabe: the Schutzbot CI will fail here. We'll have to merge this and https://github.com/osbuild/manifest-db/pull/101 at the same time. 
dustymabe: LGTM
mvo5: We will need https://github.com/osbuild/manifest-db/pull/104 for the manifest tests to turn fully green (and an update to Schutbot and the manifest-db hash)
dustymabe: > We will need [osbuild/manifest-db#104](https://github.com/osbuild/manifest-db/pull/104) for the manifest tests to turn fully green (and an update to Schutbot and the manifest-db hash)

done

Patch:
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index ce3b215a..e3dbad51 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -14,6 +14,7 @@ RUN dnf install -y \
     lsof \
     make \
     packit \
+    parted \
     qemu-img \
     qemu-system-x86 \
     osbuild \
diff --git a/Schutzfile b/Schutzfile
index 0db2f00f..8848b584 100644
--- a/Schutzfile
+++ b/Schutzfile
@@ -2,7 +2,7 @@
   "global": {
     "dependencies": {
       "manifest-db": {
-        "commit": "4dda6c10c775d5810ea1aa1442d646f903dbedaf"
+        "commit": "3bb2cb6ec1c037dfc6a6dd57f65e245c0b3e79ae"
       }
     }
   },
diff --git a/devices/org.osbuild.loopback b/devices/org.osbuild.loopback
index 0b6591d0..b98ddfea 100755
--- a/devices/org.osbuild.loopback
+++ b/devices/org.osbuild.loopback
@@ -19,7 +19,9 @@ documentation for `osbuil.util.udev.UdevInhibitor`.
 
 import argparse
 import errno
+import glob
 import os
+import stat
 import sys
 from typing import Dict
 
@@ -35,6 +37,10 @@ SCHEMA = """
     "type": "string",
     "description": "File to associate with the loopback device"
   },
+  "partscan": {
+    "type": "boolean",
+    "description": "Perform partition scanning after device creation"
+  },
   "start": {
     "type": "number",
     "description": "Start of the data segment (in sectors)",
@@ -71,10 +77,9 @@ class LoopbackService(devices.DeviceService):
         lock = UdevInhibitor.for_device(lo.LOOP_MAJOR, lo.minor)
         lo.on_close = lambda _l: lock.release()
 
-    def make_loop(self, fd: int, offset, sizelimit, lock):
+    def make_loop(self, fd: int, offset, sizelimit, lock, partscan):
         if not sizelimit:
-            stat = os.fstat(fd)
-            sizelimit = stat.st_size - offset
+            sizelimit = os.fstat(fd).st_size - offset
         else:
             sizelimit *= self.sector_size
 
@@ -83,7 +88,7 @@ class LoopbackService(devices.DeviceService):
                                   offset=offset,
                                   sizelimit=sizelimit,
                                   blocksize=self.sector_size,
-                                  partscan=False,
+                                  partscan=partscan,
                                   autoclear=True)
 
         return lo
@@ -94,12 +99,13 @@ class LoopbackService(devices.DeviceService):
         start = options.get("start", 0) * self.sector_size
         size = options.get("size")
         lock = options.get("lock", False)
+        partscan = options.get("partscan", False)
 
         path = os.path.join(tree, filename.lstrip("/"))
 
         self.fd = os.open(path, os.O_RDWR | os.O_CLOEXEC)
         try:
-            self.lo = self.make_loop(self.fd, start, size, lock)
+            self.lo = self.make_loop(self.fd, start, size, lock, partscan)
         except Exception as error:  # pylint: disable: broad-except
             self.close()
             raise error from None
@@ -111,6 +117,15 @@ class LoopbackService(devices.DeviceService):
             dir_fd = os.open(devpath, os.O_CLOEXEC | os.O_PATH)
             with ctx.suppress_oserror(errno.EEXIST):
                 self.lo.mknod(dir_fd)
+            # If partscan was enabled let's find any partition
+            # based devices that were added (i.e. loop0p1) and
+            # copy them into our custom /dev/ directory:
+            if partscan:
+                for device in glob.glob(os.path.join("/dev/", f"{self.lo.devname}p*")):
+                    os.mknod(os.path.basename(device),
+                             mode=(0o600 | stat.S_IFBLK),
+                             device=os.stat(device).st_rdev,
+                             dir_fd=dir_fd)
         finally:
             if dir_fd > -1:
                 os.close(dir_fd)
diff --git a/mounts/org.osbuild.btrfs b/mounts/org.osbuild.btrfs
index 4a5bff9f..a2b862a3 100755
--- a/mounts/org.osbuild.btrfs
+++ b/mounts/org.osbuild.btrfs
@@ -21,6 +21,10 @@ SCHEMA_2 = """
   "source": {
     "type": "string"
   },
+  "partition": {
+    "description": "If the source device has partitions, the partition number, starting at one",
+    "type": "number"
+  },
   "target": {
     "type": "string"
   },
diff --git a/mounts/org.osbuild.ext4 b/mounts/org.osbuild.ext4
index 74eb58bf..333f807f 100755
--- a/mounts/org.osbuild.ext4
+++ b/mounts/org.osbuild.ext4
@@ -21,6 +21,10 @@ SCHEMA_2 = """
   "source": {
     "type": "string"
   },
+  "partition": {
+    "description": "If the source device has partitions, the partition number, starting at one",
+    "type": "number"
+  },
   "target": {
     "type": "string"
   },
diff --git a/mounts/org.osbuild.fat b/mounts/org.osbuild.fat
index 6ea54f61..37c966e1 100755
--- a/mounts/org.osbuild.fat
+++ b/mounts/org.osbuild.fat
@@ -24,6 +24,10 @@ SCHEMA_2 = """
   "target": {
     "type": "string"
   },
+  "partition": {
+    "description": "If the source device has partitions, the partition number, starting at one",
+    "type": "number"
+  },
   "options": {
     "type": "object",
     "additionalProperties": false,
diff --git a/mounts/org.osbuild.noop b/mounts/org.osbuild.noop
index 0dea2814..7f81c36d 100755
--- a/mounts/org.osbuild.noop
+++ b/mounts/org.osbuild.noop
@@ -21,6 +21,9 @@ SCHEMA_2 = """
   "source": {
     "type": "string"
   },
+  "partition": {
+    "type": "number"
+  },
   "target": {
     "type": "string"
   },
diff --git a/mounts/org.osbuild.xfs b/mounts/org.osbuild.xfs
index 1e294325..6bc34eaf 100755
--- a/mounts/org.osbuild.xfs
+++ b/mounts/org.osbuild.xfs
@@ -24,6 +24,10 @@ SCHEMA_2 = """
   "target": {
     "type": "string"
   },
+  "partition": {
+    "description": "If the source device has partitions, the partition number, starting at one",
+    "type": "number"
+  },
   "options": {
     "type": "object",
     "additionalProperties": false,
diff --git a/osbuild/formats/v2.py b/osbuild/formats/v2.py
index c0366d5e..775be12c 100644
--- a/osbuild/formats/v2.py
+++ b/osbuild/formats/v2.py
@@ -84,9 +84,10 @@ def describe(manifest: Manifest, *, with_id=False) -> Dict:
 
         if mnt.device:
             desc["source"] = mnt.device.name
-
         if mnt.options:
             desc["options"] = mnt.options
+        if mnt.partition:
+            desc["partition"] = mnt.partition
         return desc
 
     def describe_mounts(mounts: Dict):
@@ -286,6 +287,7 @@ def load_mount(description: Dict, index: Index, stage: Stage):
         raise ValueError(f"Duplicated mount '{name}'")
 
     source = description.get("source")
+    partition = description.get("partition")
     target = description.get("target")
 
     options = description.get("options", {})
@@ -296,7 +298,7 @@ def load_mount(description: Dict, index: Index, stage: Stage):
         if not device:
             raise ValueError(f"Unknown device '{source}' for mount '{name}'")
 
-    stage.add_mount(name, info, device, target, options)
+    stage.add_mount(name, info, device, partition, target, options)
 
 
 def load_stage(description: Dict, index: Index, pipeline: Pipeline, manifest: Manifest, source_refs):
diff --git a/osbuild/mounts.py b/osbuild/mounts.py
index 76a143cc..86576ad0 100644
--- a/osbuild/mounts.py
+++ b/osbuild/mounts.py
@@ -23,10 +23,11 @@ class Mount:
     A single mount with its corresponding options
     """
 
-    def __init__(self, name, info, device, target, options: Dict):
+    def __init__(self, name, info, device, partition, target, options: Dict):
         self.name = name
         self.info = info
         self.device = device
+        self.partition = partition
         self.target = target
         self.options = options
         self.id = self.calc_id()
@@ -36,6 +37,8 @@ class Mount:
         m.update(json.dumps(self.info.name, sort_keys=True).encode())
         if self.device:
             m.update(json.dumps(self.device.id, sort_keys=True).encode())
+        if self.partition:
+            m.update(json.dumps(self.partition, sort_keys=True).encode())
         if self.target:
             m.update(json.dumps(self.target, sort_keys=True).encode())
         m.update(json.dumps(self.options, sort_keys=True).encode())
@@ -71,6 +74,11 @@ class MountManager:
         if relpath and os.path.exists(os.path.join('/dev', relpath)):
             source = os.path.join('/dev', relpath)
 
+        # If the user specified a partition then the filesystem to
+        # mount is actually on a partition of the disk.
+        if source and mount.partition:
+            source = f"{source}p{mount.partition}"
+
         root = os.fspath(self.root)
 
         args = {
diff --git a/osbuild/pipeline.py b/osbuild/pipeline.py
index 92b50dcf..a450d791 100644
--- a/osbuild/pipeline.py
+++ b/osbuild/pipeline.py
@@ -111,8 +111,8 @@ class Stage:
         self.devices[name] = dev
         return dev
 
-    def add_mount(self, name, info, device, target, options):
-        mount = Mount(name, info, device, target, options)
+    def add_mount(self, name, info, device, partition, target, options):
+        mount = Mount(name, info, device, partition, target, options)
         self.mounts[name] = mount
         return mount
 
diff --git a/schemas/osbuild2.json b/schemas/osbuild2.json
index 96795c53..562fb777 100644
--- a/schemas/osbuild2.json
+++ b/schemas/osbuild2.json
@@ -82,6 +82,9 @@
         "target": {
           "type": "string"
         },
+        "partition": {
+          "type": "number"
+        },
         "options": {
           "type": "object",
           "additionalProperties": true
diff --git a/tools/osbuild-mpp b/tools/osbuild-mpp
index ec66c6df..fbc0c8ff 100755
--- a/tools/osbuild-mpp
+++ b/tools/osbuild-mpp
@@ -831,6 +831,7 @@ class Partition:
         self.uuid = uuid
         self.attrs = attrs
         self.index = None
+        self.partnum = None
 
     @property
     def start_in_bytes(self):
@@ -936,6 +937,7 @@ class PartitionTable:
         assert len(disk_parts) == len(self.partitions)
         for i, part in enumerate(self.partitions):
             part.index = i
+            part.partnum = i + 1
             part.start = disk_parts[i]["start"]
             part.size = disk_parts[i]["size"]
             part.type = disk_parts[i].get("type")


Test patch:
diff --git a/test/data/manifests/fedora-coreos-container.json b/test/data/manifests/fedora-coreos-container.json
index f307a33c..e253a61e 100644
--- a/test/data/manifests/fedora-coreos-container.json
+++ b/test/data/manifests/fedora-coreos-container.json
@@ -682,28 +682,11 @@
             ]
           },
           "devices": {
-            "efi": {
+            "disk": {
               "type": "org.osbuild.loopback",
               "options": {
                 "filename": "disk.img",
-                "start": 4096,
-                "size": 260096
-              }
-            },
-            "boot": {
-              "type": "org.osbuild.loopback",
-              "options": {
-                "filename": "disk.img",
-                "start": 264192,
-                "size": 786432
-              }
-            },
-            "root": {
-              "type": "org.osbuild.loopback",
-              "options": {
-                "filename": "disk.img",
-                "start": 1050624,
-                "size": 4194304
+                "partscan": true
               }
             }
           },
@@ -711,19 +694,22 @@
             {
               "name": "root",
               "type": "org.osbuild.xfs",
-              "source": "root",
+              "source": "disk",
+              "partition": 4,
               "target": "/"
             },
             {
               "name": "boot",
               "type": "org.osbuild.ext4",
-              "source": "boot",
+              "source": "disk",
+              "partition": 3,
               "target": "/boot"
             },
             {
               "name": "efi",
               "type": "org.osbuild.fat",
-              "source": "efi",
+              "source": "disk",
+              "partition": 2,
               "target": "/boot/efi"
             }
           ]
@@ -884,30 +870,11 @@
             ]
           },
           "devices": {
-            "efi": {
-              "type": "org.osbuild.loopback",
-              "options": {
-                "filename": "disk.img",
-                "start": 512,
-                "size": 32512,
-                "sector-size": 4096
-              }
-            },
-            "boot": {
-              "type": "org.osbuild.loopback",
-              "options": {
-                "filename": "disk.img",
-                "start": 33024,
-                "size": 98304,
-                "sector-size": 4096
-              }
-            },
-            "root": {
+            "disk": {
               "type": "org.osbuild.loopback",
               "options": {
                 "filename": "disk.img",
-                "start": 131328,
-                "size": 524288,
+                "partscan": true,
                 "sector-size": 4096
               }
             }
@@ -916,19 +883,22 @@
             {
               "name": "root",
               "type": "org.osbuild.xfs",
-              "source": "root",
+              "source": "disk",
+              "partition": 4,
               "target": "/"
             },
             {
               "name": "boot",
               "type": "org.osbuild.ext4",
-              "source": "boot",
+              "source": "disk",
+              "partition": 3,
               "target": "/boot"
             },
             {
               "name": "efi",
               "type": "org.osbuild.fat",
-              "source": "efi",
+              "source": "disk",
+              "partition": 2,
               "target": "/boot/efi"
             }
           ]
diff --git a/test/data/manifests/fedora-coreos-container.mpp.yaml b/test/data/manifests/fedora-coreos-container.mpp.yaml
index b07d5816..aefda0d9 100644
--- a/test/data/manifests/fedora-coreos-container.mpp.yaml
+++ b/test/data/manifests/fedora-coreos-container.mpp.yaml
@@ -207,42 +207,29 @@ pipelines:
             - from: input://tree/
               to: mount://root/
         devices:
-          efi:
+          disk:
             type: org.osbuild.loopback
             options:
               filename: disk.img
-              start:
-                mpp-format-int: '{image.layout[''EFI-SYSTEM''].start}'
-              size:
-                mpp-format-int: '{image.layout[''EFI-SYSTEM''].size}'
-          boot:
-            type: org.osbuild.loopback
-            options:
-              filename: disk.img
-              start:
-                mpp-format-int: '{image.layout[''boot''].start}'
-              size:
-                mpp-format-int: '{image.layout[''boot''].size}'
-          root:
-            type: org.osbuild.loopback
-            options:
-              filename: disk.img
-              start:
-                mpp-format-int: '{image.layout[''root''].start}'
-              size:
-                mpp-format-int: '{image.layout[''root''].size}'
+              partscan: true
         mounts:
           - name: root
             type: org.osbuild.xfs
-            source: root
+            source: disk
+            partition:
+              mpp-format-int: '{image.layout[''root''].partnum}'
             target: /
           - name: boot
             type: org.osbuild.ext4
-            source: boot
+            source: disk
+            partition:
+              mpp-format-int: '{image.layout[''boot''].partnum}'
             target: /boot
           - name: efi
             type: org.osbuild.fat
-            source: efi
+            source: disk
+            partition:
+              mpp-format-int: '{image.layout[''EFI-SYSTEM''].partnum}'
             target: /boot/efi
       - type: org.osbuild.grub2.inst
         options:
@@ -339,48 +326,31 @@ pipelines:
             - from: input://tree/
               to: mount://root/
         devices:
-          efi:
-            type: org.osbuild.loopback
-            options:
-              filename: disk.img
-              start:
-                mpp-format-int: '{image4k.layout[''EFI-SYSTEM''].start}'
-              size:
-                mpp-format-int: '{image4k.layout[''EFI-SYSTEM''].size}'
-              sector-size:
-                  mpp-format-int: "{four_k_sector_size}"
-          boot:
-            type: org.osbuild.loopback
-            options:
-              filename: disk.img
-              start:
-                mpp-format-int: '{image4k.layout[''boot''].start}'
-              size:
-                mpp-format-int: '{image4k.layout[''boot''].size}'
-              sector-size:
-                  mpp-format-int: "{four_k_sector_size}"
-          root:
+          disk:
             type: org.osbuild.loopback
             options:
               filename: disk.img
-              start:
-                mpp-format-int: '{image4k.layout[''root''].start}'
-              size:
-                mpp-format-int: '{image4k.layout[''root''].size}'
+              partscan: true
               sector-size:
                   mpp-format-int: "{four_k_sector_size}"
         mounts:
           - name: root
             type: org.osbuild.xfs
-            source: root
+            source: disk
+            partition:
+              mpp-format-int: '{image4k.layout[''root''].partnum}'
             target: /
           - name: boot
             type: org.osbuild.ext4
-            source: boot
+            source: disk
+            partition:
+              mpp-format-int: '{image4k.layout[''boot''].partnum}'
             target: /boot
           - name: efi
             type: org.osbuild.fat
-            source: efi
+            source: disk
+            partition:
+              mpp-format-int: '{image4k.layout[''EFI-SYSTEM''].partnum}'
             target: /boot/efi
   - name: raw-metal-image
     build: name:build
diff --git a/test/mod/test_fmt_v2.py b/test/mod/test_fmt_v2.py
index de8e92ba..65f7d3ba 100644
--- a/test/mod/test_fmt_v2.py
+++ b/test/mod/test_fmt_v2.py
@@ -80,6 +80,13 @@ BASIC_PIPELINE = {
                                 "filename": "empty.img"
                             }
                         },
+                        "var": {
+                            "type": "org.osbuild.loopback",
+                            "options": {
+                                "filename": "empty.img",
+                                "partscan": True,
+                            },
+                        },
                     },
                     "mounts": [
                         {
@@ -93,6 +100,13 @@ BASIC_PIPELINE = {
                             "type": "org.osbuild.noop",
                             "source": "boot",
                             "target": "/boot",
+                        },
+                        {
+                            "name": "var",
+                            "type": "org.osbuild.noop",
+                            "source": "var",
+                            "target": "/var",
+                            "partition": 1,
                         }
                     ]
                 }
@@ -342,6 +356,48 @@ class TestFormatV2(unittest.TestCase):
         with self.assertRaises(ValueError):
             self.load_manifest(pipeline)
 
+    def test_mounts_with_partition(self):
+        BASE = {
+            "version": "2",
+            "pipelines": [
+                {
+                    "name": "test",
+                    "runner": "org.osbuild.linux",
+                    "stages": [
+                        {
+                            "type": "org.osbuild.noop",
+                            "options": {"zero": 0},
+                            "devices": {
+                                "root": {
+                                    "type": "org.osbuild.loopback",
+                                    "options": {
+                                        "filename": "empty.img",
+                                        "partscan": True,
+                                    }
+                                }
+                            },
+                            "mounts": [
+                                {
+                                    "name": "root",
+                                    "type": "org.osbuild.noop",
+                                    "source": "root",
+                                    "target": "/",
+                                    "partition": 1,
+                                },
+                            ],
+                        }
+                    ]
+                }
+            ]
+        }
+
+        # verify the device
+        pipeline = copy.deepcopy(BASE)
+        manifest, _ = self.load_manifest(pipeline)
+        root_mnt = manifest["test"].stages[0].mounts["root"]
+        self.assertEqual(root_mnt.device.name, "root")
+        self.assertEqual(root_mnt.partition, 1)
+
     def test_device_sorting(self):
         fmt = self.index.get_format_info("osbuild.formats.v2").module
         assert fmt
diff --git a/test/mod/test_mounts.py b/test/mod/test_mounts.py
new file mode 100644
index 00000000..416706a4
--- /dev/null
+++ b/test/mod/test_mounts.py
@@ -0,0 +1,22 @@
+from unittest.mock import Mock
+
+import pytest
+
+from osbuild.mounts import Mount
+from osbuild.meta import ModuleInfo
+
+
+def test_mount_calc_id_is_stable():
+    info = Mock(spec=ModuleInfo)
+    info.name = "some-name"
+    device = Mock(spec=ModuleInfo)
+    device.id = "some-id"
+    partition = 1
+    target = "/"
+    opts = {"opt1": 1}
+    # make sure to update Mount.calc_id and this test when adding
+    # parameters here
+    mount1 = Mount("name", info, device, partition, target, opts)
+    assert mount1.id == "15066da9ff760a60f1d1a360de2ad584cc0c97d6f6034e3258b3275ba3da6bb2"
+    mount2 = Mount("name", info, device, partition, target, opts)
+    assert mount1.id == mount2.id
diff --git a/test/run/test_mount.py b/test/run/test_mount.py
index 005ad299..3c30dec4 100755
--- a/test/run/test_mount.py
+++ b/test/run/test_mount.py
@@ -7,6 +7,7 @@
 import errno
 import json
 import os
+import pathlib
 import subprocess
 import sys
 import tempfile
@@ -15,6 +16,7 @@ from contextlib import contextmanager
 import pytest
 
 from osbuild import devices, host, meta, mounts
+from osbuild import testutil
 
 from ..test import TestBase
 
@@ -107,6 +109,7 @@ def mount(mgr, devpath, tree, size, mountpoint, options):
             lpath,
             index.get_module_info("Mount", "org.osbuild.fat"),
             dev,
+            None,
             "/",
             options
         )
@@ -164,3 +167,64 @@ def test_all_options(tmp_path):
                         assert "winnt" in strline
                         shortname_tested = True
                 assert shortname_tested
+
+
+def create_image_with_partitions(tmp_path):
+    tree = tmp_path / "tree"
+    tree.mkdir()
+    img = tree / "disk.img"
+    with img.open("w") as fp:
+        fp.truncate(20*1024*1024)
+    for cmd in [
+        ["parted", "--script", img, "mklabel", "msdos"],
+        ["parted", "--script", img, "mkpart", "primary", "ext4", "1MiB", "10Mib"],
+        ["parted", "--script", img, "mkpart", "primary", "ext4", "10MiB", "19Mib"],
+        ["mkfs.ext4", "-E", f"offset={1*1024*1024}", img, "9M"],
+        ["mkfs.ext4", "-E", f"offset={10*1024*1024}", img, "9M"],
+    ]:
+        subprocess.check_call(cmd)
+    return tree, img.name
+
+
+@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
+@pytest.mark.skipif(not testutil.has_executable("parted"), reason="no parted executable")
+def test_mount_with_partition(tmp_path):
+    tree, img_name = create_image_with_partitions(tmp_path)
+
+    # need root of the sourcecode to find the stages/mounts etc
+    src_root = pathlib.Path(__file__).parent.parent.parent
+    index = meta.Index(src_root)
+    loopback_mod_info = index.get_module_info("Device", "org.osbuild.loopback")
+    ext4_mod_info = index.get_module_info("Mount", "org.osbuild.ext4")
+
+    with host.ServiceManager() as mgr:
+        with make_dev_tmpfs(tmp_path) as devpath:
+            devmgr = devices.DeviceManager(mgr, devpath, tree)
+            opts = {
+                "filename": img_name,
+                "partscan": True,
+            }
+            dev = devices.Device("loop", loopback_mod_info, None, opts)
+            device_node_path = os.path.join(devpath, devmgr.open(dev)["path"])
+
+            mnt_base = tmp_path / "mntbase"
+            mntmgr = mounts.MountManager(devmgr, mnt_base)
+            opts = {}
+            # mount both partitions
+            for i in range(1,3):
+                mount = mounts.Mount(
+                    name=f"name-{i}", info=ext4_mod_info, device=dev,
+                    partition=i, target=f"/mnt-{i}", options=opts)
+                mntmgr.mount(mount)
+
+            # check that the both mounts actually happend
+            output = subprocess.check_output(
+                ["lsblk", "--json", device_node_path],
+                encoding="utf8",
+            ).strip()
+            lsblk_info = json.loads(output)
+            assert len(lsblk_info["blockdevices"][0]["children"]) == 2
+            chld = lsblk_info["blockdevices"][0]["children"]
+            assert chld[0]["mountpoints"] == [f"{mnt_base}/mnt-1"]
+            assert chld[1]["mountpoints"] == [f"{mnt_base}/mnt-2"]
+