From 0ac83fd421ac669cb94f22f74fc32ac8167dbe70 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Tue, 13 May 2025 12:35:01 +0200 Subject: [PATCH] stages/kickstart: post-installation scripts Add a new %post option to the kickstart stage that supports adding multiple post blocks to a kickstart file, with all the options supported by the directive. --- stages/org.osbuild.kickstart | 22 +++++++++++++ stages/org.osbuild.kickstart.meta.json | 37 +++++++++++++++++++++ stages/test/test_kickstart.py | 45 ++++++++++++++++++++++++++ 3 files changed, 104 insertions(+) diff --git a/stages/org.osbuild.kickstart b/stages/org.osbuild.kickstart index f8d867ed..01b24152 100755 --- a/stages/org.osbuild.kickstart +++ b/stages/org.osbuild.kickstart @@ -170,6 +170,25 @@ def make_network(options: Dict) -> List[str]: return res +def make_post(post_list): + res = [] + for post in post_list: + start = ["%post"] + if post.get("erroronfail"): + start.append("--erroronfail") + if post.get("nochroot"): + start.append("--nochroot") + log = post.get("log") + if log: + start.extend(["--log", f'"{log}"']) + interpreter = post.get("interpreter") + if interpreter: + start.extend(["--interpreter", f'"{interpreter}"']) + + res.extend([" ".join(start), *post["commands"], "%end"]) + return res + + def main(tree, options): # pylint: disable=too-many-branches path = options["path"].lstrip("/") ostree = options.get("ostree") @@ -242,6 +261,9 @@ def main(tree, options): # pylint: disable=too-many-branches kargs_append = options.get("bootloader", {}).get("append") if kargs_append: config += [f"bootloader --append='{kargs_append}'"] + post = options.get("%post", []) + if post: + config += make_post(post) target = os.path.join(tree, path) base = os.path.dirname(target) diff --git a/stages/org.osbuild.kickstart.meta.json b/stages/org.osbuild.kickstart.meta.json index bd9bd9d0..d69dbd85 100644 --- a/stages/org.osbuild.kickstart.meta.json +++ b/stages/org.osbuild.kickstart.meta.json @@ -555,6 +555,43 @@ "type": "string" } } + }, + "%post": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "description": "Adds arbitrary commands to run on the system once the installation is complete.", + "additionalProperties": false, + "required": [ + "commands" + ], + "properties": { + "erroronfail": { + "description": "Stop the installation on script failure", + "type": "boolean" + }, + "interpreter": { + "description": "Allows specifying a different scripting language", + "type": "string" + }, + "log": { + "description": "Log all messages from the script to the given log file", + "type": "string" + }, + "nochroot": { + "description": "Run outside of the chroot environment", + "type": "boolean" + }, + "commands": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + } } } } diff --git a/stages/test/test_kickstart.py b/stages/test/test_kickstart.py index a635973d..7ccd848c 100644 --- a/stages/test/test_kickstart.py +++ b/stages/test/test_kickstart.py @@ -241,6 +241,47 @@ TEST_INPUT = [ ({"ostreecontainer": {"transport": "dir", "url": "/run/install/repo/container", }, }, "ostreecontainer --url=/run/install/repo/container --transport=dir",), ({"bootloader": {"append": "karg1 karg2=0"}}, "bootloader --append='karg1 karg2=0'"), + + # %post + ({"%post": [{"commands": ["mkdir /scratch"]}]}, "%post\nmkdir /scratch\n%end"), + ( + { + "%post": [ + {"commands": ["mkdir /scratch"]}, + {"commands": ["print('DONE!!!')"], "interpreter": "/usr/bin/python3"}, + ] + }, + "%post\nmkdir /scratch\n%end\n" + + "%post --interpreter \"/usr/bin/python3\"\nprint('DONE!!!')\n%end" + ), + ( + { + "%post": [ + {"commands": ["mkdir /scratch"]}, + { + "erroronfail": True, + "nochroot": True, + "interpreter": "/usr/bin/bash", + "log": "/mnt/sysimage/var/log/ks-p2.log", + "commands": [ + "echo 'Starting post2'", + "if [ ! -e /mnt/sysimage/etc/resolv.conf ]; then", + " cp /etc/resolv.conf /mnt/sysimage/etc/resolv.conf", + "fi", + ] + }, + {"commands": ["print('DONE!!!')"], "interpreter": "/usr/bin/python3"}, + ] + }, + "%post\nmkdir /scratch\n%end\n" + + "%post --erroronfail --nochroot --log \"/mnt/sysimage/var/log/ks-p2.log\" --interpreter \"/usr/bin/bash\"\n" + + "echo 'Starting post2'\n" + + "if [ ! -e /mnt/sysimage/etc/resolv.conf ]; then\n" + + " cp /etc/resolv.conf /mnt/sysimage/etc/resolv.conf\n" + + "fi\n" + + "%end\n" + + "%post --interpreter \"/usr/bin/python3\"\nprint('DONE!!!')\n%end" + ) ] @@ -387,6 +428,10 @@ def test_kickstart_valid(tmp_path, stage_module, test_input, expected): # pylin {"rootpw": {"iscrypted": True, "plaintext": True, "password": "pass"}}, "is not valid under any of the given schemas" ), + # bad %post blocks + ({"%post": []}, re.compile(r"\[\] should be non-empty|\[\] is too short")), + ({"%post": [{}]}, "'commands' is a required property"), + ({"%post": [{"commands": []}]}, re.compile(r"\[\] should be non-empty|\[\] is too short")), ], ) @pytest.mark.parametrize("stage_schema", ["1"], indirect=True)