From 3f8e100988bc89f78d63a8d7b3a3c2dcf2be68be Mon Sep 17 00:00:00 2001 From: Nikita Dubrovskii Date: Mon, 30 Sep 2024 15:46:31 +0200 Subject: [PATCH] org.osbuild.mkdir: support creating dirs on mounts This allows creating new directories on mounts: ``` - type: org.osbuild.mkdir options: paths: - path: mount:///boot/efi devices: disk: ... mounts: - name: boot target: /boot ... ``` --- stages/org.osbuild.mkdir | 22 ++++---- stages/org.osbuild.mkdir.meta.json | 21 ++++++- stages/test/test_mkdir.py | 89 ++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 13 deletions(-) create mode 100644 stages/test/test_mkdir.py diff --git a/stages/org.osbuild.mkdir b/stages/org.osbuild.mkdir index f04549f655..d2d11a7ad4 100755 --- a/stages/org.osbuild.mkdir +++ b/stages/org.osbuild.mkdir @@ -3,23 +3,26 @@ import os import sys import osbuild.api -from osbuild.util.path import in_tree +from osbuild.util import parsing -def main(tree, options): +def main(args): + options = args["options"] + for item in options["paths"]: path = item["path"] mode = item.get("mode", 0o777) parents = item.get("parents", False) exist_ok = item.get("exist_ok", False) - if not path.startswith("/"): - print("WARNING: relative path used, this is discouraged!") - - target = os.path.join(tree, path.lstrip("/")) - if not in_tree(target, tree): - raise ValueError(f"path {path} not in tree") + if "://" not in path: + if not path.startswith("/"): + print("WARNING: relative path used, this is discouraged!") + path = f"tree:///{path}" + else: + path = f"tree://{path}" + target = parsing.parse_location(path, args) if parents: os.makedirs(target, mode=mode, exist_ok=exist_ok) else: @@ -33,5 +36,4 @@ def main(tree, options): if __name__ == "__main__": - args = osbuild.api.arguments() - sys.exit(main(args["tree"], args["options"])) + sys.exit(main(osbuild.api.arguments())) diff --git a/stages/org.osbuild.mkdir.meta.json b/stages/org.osbuild.mkdir.meta.json index 5534120a89..6cebaaf5fb 100644 --- a/stages/org.osbuild.mkdir.meta.json +++ b/stages/org.osbuild.mkdir.meta.json @@ -1,5 +1,5 @@ { - "summary": "Create directories within the tree.", + "summary": "Create directories within the tree or mount.", "description": [ "Can create one or more directories, optionally also the", "intermediate directories. The stage can gracefully handle", @@ -31,8 +31,23 @@ ], "properties": { "path": { - "type": "string", - "pattern": "^\\/?(?!\\.\\.)((?!\\/\\.\\.\\/).)+$" + "anyOf": [ + { + "type": "string", + "description": "Target path, if a tree", + "pattern": "^\\/?(?!\\.\\.)((?!\\/\\.\\.\\/).)+$" + }, + { + "type": "string", + "description": "Target path, if a mount", + "pattern": "^mount://.+" + }, + { + "type": "string", + "description": "Target path, if a tree", + "pattern": "^tree://.+" + } + ] }, "mode": { "type": "number", diff --git a/stages/test/test_mkdir.py b/stages/test/test_mkdir.py new file mode 100644 index 0000000000..6285a380eb --- /dev/null +++ b/stages/test/test_mkdir.py @@ -0,0 +1,89 @@ +#!/usr/bin/python3 + +import contextlib +import os +import subprocess + +import pytest # type: ignore + +from osbuild.testutil import has_executable + +STAGE_NAME = "org.osbuild.mkdir" + + +def test_mkdir(tmp_path, stage_module): + tree = tmp_path / "tree" + tree.mkdir() + + options = { + "paths": [ + {"path": "/fake_dir"}, + {"path": "fake_relative_dir"} + ] + } + args = { + "tree": f"{tree}", + "options": options + } + stage_module.main(args) + assert (tree / "fake_dir").exists() + assert (tree / "fake_relative_dir").exists() + + +def test_mkdir_on_a_tree(tmp_path, stage_module): + tree = tmp_path / "tree" + tree.mkdir() + + options = { + "paths": [ + { + "path": "tree:///fake_parent/fake_dir", + "parents": 1 + } + ] + } + args = { + "tree": f"{tree}", + "options": options + } + stage_module.main(args) + assert (tree / "fake_parent/fake_dir").exists() + + +@pytest.mark.skipif(os.getuid() != 0, reason="needs root") +@pytest.mark.skipif(not has_executable("mkfs.ext4"), reason="need mkfs.ext4") +def test_mkdir_on_a_mount(tmp_path, stage_module): + tree = tmp_path / "tree" + tree.mkdir() + + # Create fake EXT4 disk image + fake_disk_path = tmp_path / "fake.img" + with fake_disk_path.open("w") as fp: + fp.truncate(10 * 1024 * 1024) + subprocess.run( + ["mkfs.ext4", os.fspath(fake_disk_path)], check=True) + + fake_disk_mnt = tmp_path / "mounts" + fake_disk_mnt.mkdir() + + with contextlib.ExitStack() as cm: + subprocess.run(["mount", fake_disk_path, fake_disk_mnt], check=True) + cm.callback(subprocess.run, ["umount", fake_disk_mnt], check=True) + + options = { + "paths": [ + { + "path": "mount:///fake_parent/fake_dir", + "parents": 1 + } + ] + } + args = { + "tree": f"{tree}", + "options": options, + "paths": { + "mounts": fake_disk_mnt, + } + } + stage_module.main(args) + assert (fake_disk_mnt / "fake_parent/fake_dir").exists()