mounts: implement new org.osbuild.bind mount

This adds a new `org.osbuild.bind` mount feature to the osbuild
mount modules. This allows to (r)bind mount parts of another mount
into the tree (or replace the default tree for a stage entirely).

The use case is the `bootc install to-filesystem` where we get
a populated disk and need to do customizations directly there
without going through an intermediate tree.

Note that right now only "--rbind" is supported and used but
we could trivially change that to become an option in either
direction. Given that the main use-case right now is to be
paried with `org.osbuild.ostree.deployment` and here the
`rbind` is crucial I would leave that the default.

Here is an example what this looks like:
```json
        {
          "type": "org.osbuild.users",
          "options": {
            "users": {
              "alice": {
                "home": "/home/alice",
                "groups": [
                  "wheel"
                ],
                "password": "$6$NV3P7UzUqP3xb1ML$3qnHpWs037VRTaOc.kirQ4.RwNz4gu9dkhAhpBYVCkHw8CMhpBKnegyyqw0QfURowarZnRnQi.jo4JEzIOvPO/",
                "key": "ssh-rsa AAA ... user@email.com"
              }
            }
          },
          "devices": {
            "disk": {
              "type": "org.osbuild.loopback",
              "options": {
                "filename": "disk.raw",
                "partscan": true
              }
            }
          },
          "mounts": [
            {
              "name": "part4",
              "type": "org.osbuild.ext4",
              "source": "disk",
              "target": "/",
              "partition": 4
            },
            ...
            {
              "name": "ostree.deployment",
              "type": "org.osbuild.ostree.deployment",
              "options": {
                "source": "mount",
                "deployment": {
                  "default": true
                }
              }
            },
            {
              "name": "bind",
              "type": "org.osbuild.bind",
	      "target": "tree://",
	      "options": {
		"source": "mount://"
	      }
            }
          ]
        },
```
This commit is contained in:
Michael Vogt 2024-04-09 10:04:06 +02:00 committed by Ondřej Budai
parent d504165c80
commit a4dfd2614f
2 changed files with 186 additions and 0 deletions

86
mounts/org.osbuild.bind Executable file
View file

@ -0,0 +1,86 @@
#!/usr/bin/python3
"""
Bind mount service
Can (r)bind mount mounts to the tree.
"""
import os.path
import subprocess
import sys
from typing import Dict
from urllib.parse import urlparse
from osbuild import mounts
SCHEMA_2 = """
"additionalProperties": false,
"required": ["name", "type", "target"],
"properties": {
"name": { "type": "string" },
"type": { "type": "string" },
"target": {
"type": "string",
"pattern": "^tree://"
},
"options": {
"required": ["source"],
"source": {
"type": "string",
"pattern": "^mount://"
}
}
}
"""
def parse_location(location, tree, mountroot: str) -> str:
# we cannot use "osutil.util.parsing" here because it is too
# tightly coupled with how arguments for stages are passed
url = urlparse(location)
path = url.netloc
if url.scheme == "tree":
return os.path.join(tree, path.rstrip("/"))
if url.scheme == "mount":
return os.path.join(mountroot, path.rstrip("/"))
raise ValueError(f"unsupported schema {url.scheme} for {location}")
class BindMount(mounts.MountService):
def __init__(self, args):
super().__init__(args)
self.mountpoint = ""
def mount(self, args: Dict):
tree = args["tree"]
mountroot = args["root"]
target = args["target"]
# we cannot use args["sources"] here because the osbuild code makes
# many assumptions about that it must link back to a "Device" so
# we follow the pattern from org.osbuild.ostree.deployment here
# and put it into "options"
options = args["options"]
source = parse_location(options.get("source"), tree, mountroot)
self.mountpoint = parse_location(target, tree, mountroot)
subprocess.run([
"mount",
"--rbind", source, self.mountpoint,
], check=True)
def umount(self):
if self.mountpoint:
subprocess.run(["umount", "-R", "-v", self.mountpoint], check=True)
self.mountpoint = ""
def sync(self):
pass
def main():
service = BindMount.from_args(sys.argv[1:])
service.main()
if __name__ == '__main__':
main()