From 8084ef869f2cc276ad77af8659fe7d91898cda6d Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 9 Nov 2023 10:59:35 +0100 Subject: [PATCH] stages(kickstart): implement "reboot" option This commit implements the `reboot` option for kickstart files. Note that there are two ways this can be enabled via the json. Either via a boolean or by passing a dict with options. ``` {"reboot": true} {"reboot": {"eject": true, "kexec": true} ``` Note that passing the empty dict as in: ``` {"reboot": {}} ``` will *not* write out a reboot line and the code also changes "clearpart" have have the same behavior to be consistent. I can see arguments that passing the empty dict should still enable `reboot` without options. Happy to change it if someone has strong(er) opinions. --- stages/org.osbuild.kickstart | 39 ++++++++++++++++++++++++++++++++++- stages/test/test_kickstart.py | 18 ++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/stages/org.osbuild.kickstart b/stages/org.osbuild.kickstart index 82f949c173..a1793e992e 100755 --- a/stages/org.osbuild.kickstart +++ b/stages/org.osbuild.kickstart @@ -170,6 +170,26 @@ SCHEMA = r""" "type": "boolean" } } + }, + "reboot": { + "description": "Reboot after the installation is successfully completed", + "oneOf": [ + { + "type": "boolean" + }, { + "type": "object", + "additionalProperties": false, + "properties": { + "eject": { + "description": "Attempt to eject the installation media before rebooting", + "type": "boolean" + }, + "kexec": { + "description": "Use this option to reboot into the new system using the kexec", + "type": "boolean" + } + } + }] } } """ @@ -238,7 +258,7 @@ def make_users(users: Dict) -> List[str]: def make_clearpart(options: Dict) -> str: clearpart = options.get("clearpart") - if clearpart is None: + if not clearpart: return "" cmd = "clearpart" al = clearpart.get("all", False) @@ -259,6 +279,19 @@ def make_clearpart(options: Dict) -> str: return cmd +def make_reboot(options): + reboot = options.get("reboot", None) + if not reboot: + return "" + cmd = "reboot" + if isinstance(reboot, dict): + if reboot.get("eject"): + cmd += " --eject" + if reboot.get("kexec"): + cmd += " --kexec" + return cmd + + def main(tree, options): path = options["path"].lstrip("/") ostree = options.get("ostree") @@ -302,6 +335,10 @@ def main(tree, options): if clearpart: config += [clearpart] + reboot = make_reboot(options) + if reboot: + config += [reboot] + target = os.path.join(tree, path) base = os.path.dirname(target) os.makedirs(base, exist_ok=True) diff --git a/stages/test/test_kickstart.py b/stages/test/test_kickstart.py index 1bd64ace68..649c5ec707 100644 --- a/stages/test/test_kickstart.py +++ b/stages/test/test_kickstart.py @@ -55,7 +55,8 @@ 'sshkey --username someusr "ssh-rsa not-really-a-real-key"' ), ({"zerombr": "true"}, "zerombr"), - ({"clearpart": {}}, "clearpart"), + # no clearpart for an empty dict (will not do anything with options anyway) + ({"clearpart": {}}, ""), ({"clearpart": {"all": True}}, "clearpart --all"), ({"clearpart": {"drives": ["hda", "hdb"]}}, "clearpart --drives=hda,hdb",), ({"clearpart": {"drives": ["hda"]}}, "clearpart --drives=hda"), @@ -88,6 +89,14 @@ }, "lang en_US.UTF-8\nkeyboard us\ntimezone UTC\nzerombr\nclearpart --all --drives=sd*|hd*|vda,/dev/vdc", ), + # no reboot for an empty dict + ({"reboot": {}}, ""), + ({"reboot": True}, "reboot"), + ({"reboot": {"eject": False}}, "reboot"), + ({"reboot": {"eject": True}}, "reboot --eject"), + ({"reboot": {"kexec": False}}, "reboot"), + ({"reboot": {"kexec": True}}, "reboot --kexec"), + ({"reboot": {"eject": True, "kexec": True}}, "reboot --eject --kexec"), ]) def test_kickstart(tmp_path, test_input, expected): ks_stage_path = os.path.join(os.path.dirname(__file__), "../org.osbuild.kickstart") @@ -105,7 +114,8 @@ def test_kickstart(tmp_path, test_input, expected): assert ks_content == expected + "\n" # double check with pykickstart if the file looks valid - subprocess.check_call(["ksvalidator", ks_path]) + if expected: + subprocess.check_call(["ksvalidator", ks_path]) @pytest.mark.parametrize("test_data,expected_err", [ @@ -118,10 +128,14 @@ def test_kickstart(tmp_path, test_input, expected): ({"clearpart": {"list": ["\n%pre not allowed"]}}, "not allowed' does not match"), ({"clearpart": {"list": ["no,comma"]}}, "no,comma' does not match"), ({"clearpart": {"disklabel": "\n%pre not allowed"}}, "not allowed' does not match"), + ({"reboot": "random-string"}, "'random-string' is not valid "), + ({"reboot": {"random": "option"}}, "{'random': 'option'} is not valid "), # GOOD pattern we want to keep working ({"clearpart": {"drives": ["sd*|hd*|vda", "/dev/vdc"]}}, ""), ({"clearpart": {"drives": ["disk/by-id/scsi-58095BEC5510947BE8C0360F604351918"]}}, ""), ({"clearpart": {"list": ["sda2", "sda3", "sdb1"]}}, ""), + ({"reboot": True}, ""), + ({"reboot": {"kexec": False}}, ""), ]) def test_schema_validation_smoke(test_data, expected_err): name = "org.osbuild.kickstart"