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.
This commit is contained in:
Achilleas Koutsou 2025-05-13 12:35:01 +02:00 committed by Tomáš Hozza
parent 68b2301daf
commit 0ac83fd421
3 changed files with 104 additions and 0 deletions

View file

@ -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)

View file

@ -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"
}
}
}
}
}
}
}

View file

@ -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)