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 <teg@jklm.no>
This commit is contained in:
Tom Gundersen 2020-04-05 16:21:28 +02:00 committed by David Rheinsberg
parent 551faf2d61
commit c2243aee6a
4 changed files with 124 additions and 0 deletions

82
stages/org.osbuild.first-boot Executable file
View file

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

View file

@ -0,0 +1,4 @@
{
"sources": {},
"pipeline": {}
}

View file

@ -0,0 +1,16 @@
{
"sources": {},
"pipeline": {
"stages": [
{
"name": "org.osbuild.first-boot",
"options": {
"commands": [
"/usr/bin/true"
],
"wait_for_network": true
}
}
]
}
}

View file

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