From c2243aee6a47952b2b0c769ddd29316db96ca4bd Mon Sep 17 00:00:00 2001 From: Tom Gundersen Date: Sun, 5 Apr 2020 16:21:28 +0200 Subject: [PATCH] stage: add org.osbuild.first-boot This stage runs a given command only on the first boot of the image, useful for doing instantiation tasks that can only be done in the target environment, or that should be done per-instance, rather than per image. Ideally we would use systemd's ConditionFirstBoot for this, but that requires images to ship without an /etc/machine-id, and currently we only support shipping images with an empty /etc/machine-id. Changing this would mean dropping /etc/fstab in favor of mounting the rootfs rw from the initrd. This is likely the right thing to do regardless, but we would have to audit what other first-boot services we would end up with pulling in in this case. Instead we introduce our own flag file /etc/osbuild-first-boot, and use ConditionPathExists. Signed-off-by: Tom Gundersen --- stages/org.osbuild.first-boot | 82 ++++++++++++++++++++++++++ test/stages_tests/first-boot/a.json | 4 ++ test/stages_tests/first-boot/b.json | 16 +++++ test/stages_tests/first-boot/diff.json | 22 +++++++ 4 files changed, 124 insertions(+) create mode 100755 stages/org.osbuild.first-boot create mode 100644 test/stages_tests/first-boot/a.json create mode 100644 test/stages_tests/first-boot/b.json create mode 100644 test/stages_tests/first-boot/diff.json diff --git a/stages/org.osbuild.first-boot b/stages/org.osbuild.first-boot new file mode 100755 index 00000000..f3e2fb90 --- /dev/null +++ b/stages/org.osbuild.first-boot @@ -0,0 +1,82 @@ +#!/usr/bin/python3 + +import json +import os +import sys + +STAGE_DESC = "Execute commands on first-boot" +STAGE_INFO = """ +Sequentially execute a list of commands on first-boot / instantiation. + +This stage uses a logic similar to systemd's first-boot to execute a given +script only the first time the image is booted. + +An empty flag file /etc/osbuild-first-boot is written to /etc and a systemd +service is enabled that is only run when the file exits, and will remove it +before executing the given commands. + +If the flag-file cannot be removed, the service fails without executing +any further first-boot commands. +""" +STAGE_OPTS = """ +"required": ["commands"], +"properties": { + "commands": { + "type": "array", + "description": "The command lines to execute", + "items": { + "type": "string" + } + }, + "wait_for_network": { + "type": "bool", + "description": "Wait for the network to be up before executing", + "default": false + } +} +""" + +def add_first_boot(tree, commands, wait_for_network): + if wait_for_network: + network = """Wants=network-online.target +After=network-online.target""" + else: + network = "" + + execs = "\n" + for command in commands: + execs += f"ExecStart={command}\n" + + service = f"""[Unit] +Description=OSBuild First Boot Service +ConditionPathExists=/etc/osbuild-first-boot +{network} + +[Service] +Type=oneshot +{execs}""" + + os.makedirs(f"{tree}/usr/lib/systemd/system/default.target.wants", exist_ok=True) + with open(f"{tree}/usr/lib/systemd/system/osbuild-first-boot.service", "w") as f: + f.write(service) + os.symlink("../osbuild-first-boot.service", + f"{tree}/usr/lib/systemd/system/default.target.wants/osbuild-first-boot.service") + + os.makedirs(f"{tree}/etc", exist_ok=True) + open(f"{tree}/etc/osbuild-first-boot", 'a').close() + +def main(tree, options): + commands = options["commands"] + wait_for_network = options.get("wait_for_network", False) + + commands = ["/usr/bin/rm /etc/osbuild-first-boot"] + commands + + add_first_boot(tree, commands, wait_for_network) + + return 0 + + +if __name__ == '__main__': + args = json.load(sys.stdin) + r = main(args["tree"], args["options"]) + sys.exit(r) diff --git a/test/stages_tests/first-boot/a.json b/test/stages_tests/first-boot/a.json new file mode 100644 index 00000000..6d256b23 --- /dev/null +++ b/test/stages_tests/first-boot/a.json @@ -0,0 +1,4 @@ +{ + "sources": {}, + "pipeline": {} +} diff --git a/test/stages_tests/first-boot/b.json b/test/stages_tests/first-boot/b.json new file mode 100644 index 00000000..acc12bbe --- /dev/null +++ b/test/stages_tests/first-boot/b.json @@ -0,0 +1,16 @@ +{ + "sources": {}, + "pipeline": { + "stages": [ + { + "name": "org.osbuild.first-boot", + "options": { + "commands": [ + "/usr/bin/true" + ], + "wait_for_network": true + } + } + ] + } +} diff --git a/test/stages_tests/first-boot/diff.json b/test/stages_tests/first-boot/diff.json new file mode 100644 index 00000000..ab5103ca --- /dev/null +++ b/test/stages_tests/first-boot/diff.json @@ -0,0 +1,22 @@ +{ + "added_files": [ + "/etc", + "/etc/osbuild-first-boot", + "/usr", + "/usr/lib", + "/usr/lib/systemd", + "/usr/lib/systemd/system", + "/usr/lib/systemd/system/default.target.wants", + "/usr/lib/systemd/system/default.target.wants/osbuild-first-boot.service", + "/usr/lib/systemd/system/osbuild-first-boot.service" + ], + "deleted_files": [], + "differences": { + "/": { + "mode": [ + 16832, + 16877 + ] + } + } +}