From 9a036078c4d2b10ef1ba43cd3e20422d6fcfdc1a Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Tue, 17 Sep 2024 13:54:54 +0200 Subject: [PATCH] Add org.osbuild.containers.unit.create stage This is essentially org.osbuild.systemd.unit.create but creates file where podman looks for quadlets instead. For now only container, volume and network is supported. Not all quadlet options are supported, but at least the most usef ones, and enough for the automotive sample-images. --- stages/org.osbuild.containers.unit.create | 61 ++++ ...g.osbuild.containers.unit.create.meta.json | 330 ++++++++++++++++++ 2 files changed, 391 insertions(+) create mode 100755 stages/org.osbuild.containers.unit.create create mode 100644 stages/org.osbuild.containers.unit.create.meta.json diff --git a/stages/org.osbuild.containers.unit.create b/stages/org.osbuild.containers.unit.create new file mode 100755 index 0000000000..23914c023c --- /dev/null +++ b/stages/org.osbuild.containers.unit.create @@ -0,0 +1,61 @@ +#!/usr/bin/python3 +import configparser +import sys + +import osbuild.api + + +def validate(filename, cfg): + # ensure the service name does not exceed maximum filename length + if len(filename) > 255: + raise ValueError(f"Error: the {filename} unit exceeds the maximum filename length.") + + # Filename extension must match the config: + # .service requires a Service section + # .mount requires a Mount section + if filename.endswith(".container") and "Container" not in cfg: + raise ValueError(f"Error: {filename} unit requires Container section") + if filename.endswith(".volume") and "Volume" not in cfg: + raise ValueError(f"Error: {filename} unit requires Volume section") + if filename.endswith(".network") and "Network" not in cfg: + raise ValueError(f"Error: {filename} unit requires Network section") + + +def main(tree, options): + filename = options["filename"] + cfg = options["config"] + validate(filename, cfg) + + # We trick configparser into letting us write multiple instances of the same option by writing them as keys with no + # value, so we enable allow_no_value + config = configparser.ConfigParser(allow_no_value=True, interpolation=None) + # prevent conversion of the option name to lowercase + config.optionxform = lambda option: option + + for section, opts in cfg.items(): + if not config.has_section(section): + config.add_section(section) + for option, value in opts.items(): + if isinstance(value, list): + for v in value: + if option == "Environment": + # Option value becomes "KEY=VALUE" (quoted) + v = '"' + v["key"] + "=" + str(v["value"]) + '"' + config.set(section, str(option) + "=" + str(v)) + else: + config.set(section, option, str(value)) + persistent = options.get("unit-path", "usr") + systemd_dir = str() + if persistent == "usr": + systemd_dir = f"{tree}/usr/share/containers/systemd" + elif persistent == "etc": + systemd_dir = f"{tree}/etc/containers/systemd" + + with open(f"{systemd_dir}/{filename}", "w", encoding="utf8") as f: + config.write(f, space_around_delimiters=False) + + +if __name__ == '__main__': + args = osbuild.api.arguments() + r = main(args["tree"], args["options"]) + sys.exit(r) diff --git a/stages/org.osbuild.containers.unit.create.meta.json b/stages/org.osbuild.containers.unit.create.meta.json new file mode 100644 index 0000000000..b1eb612cab --- /dev/null +++ b/stages/org.osbuild.containers.unit.create.meta.json @@ -0,0 +1,330 @@ +{ + "summary": "Create a podman systemd unit file", + "description": [ + "This stage allows to create Podman systemd (quadlet) unit files. The `filename` property", + "specifies the, '.service' or '.mount' file to be added. These names are", + "validated using the, same rules as specified by podman-systemd.unit(5) and they", + "must contain the, '.container', '.volume' or '.network' suffix (other types of unit files", + "are not supported). 'unit-path' determines determine the unit load path.", + "", + "The Unit configuration can currently specify the following subset", + "of options:", + " - 'Unit' section", + " - 'Description' - string", + " - 'ConditionPathExists' - string", + " - 'ConditionPathIsDirectory' - string", + " - 'DefaultDependencies' - bool", + " - 'Requires' - [strings]", + " - 'Wants' - [strings]", + " - 'After' - [strings]", + " - 'Before' - [strings]", + " - 'Service' section", + " - 'Restart' - string", + " - 'Container' section", + " - 'Image' - string", + " - 'Exec' - string", + " - 'Volume' - [string]", + " - 'User' - string", + " - 'Group' - string", + " - 'AddDevice' - string", + " - 'Environment' - [object]", + " - 'Network' - string", + " - 'WorkingDir' - string", + " - 'Volume' section", + " - 'VolumeName' - string", + " - 'Driver' - string", + " - 'Image' - string", + " - 'User' - string", + " - 'Group' - string", + " - 'Network' section", + " - 'Gateway' - string", + " - 'DNS' - string", + " - 'IPRange' - string", + " - 'Subnet' - string", + " - 'Driver' - string", + " - 'NetworkName' - string", + " - 'Install' section", + " - 'WantedBy' - [string]", + " - 'RequiredBy' - [string]" + ], + "schema": { + "additionalProperties": false, + "required": [ + "filename", + "config" + ], + "properties": { + "filename": { + "type": "string", + "pattern": "^[\\w:.\\\\-]+[@]{0,1}[\\w:.\\\\-]*\\.(container|volume|network)$" + }, + "unit-path": { + "type": "string", + "enum": [ + "usr", + "etc" + ], + "default": "usr", + "description": "Define the system load path" + }, + "config": { + "additionalProperties": false, + "type": "object", + "oneOf": [ + { + "required": [ + "Unit", + "Container", + "Install" + ], + "not": { + "required": [ + "Service", + "Volume", + "Network" + ] + } + }, + { + "required": [ + "Volume" + ], + "not": { + "required": [ + "Container", + "Network", + "Service" + ] + } + }, + { + "required": [ + "Network" + ], + "not": { + "required": [ + "Service", + "Container", + "Volume" + ] + } + } + ], + "description": "Configuration for a '.container' unit.", + "properties": { + "Unit": { + "additionalProperties": false, + "type": "object", + "description": "'Unit' configuration section of a unit file.", + "properties": { + "Description": { + "type": "string" + }, + "Wants": { + "type": "array", + "items": { + "type": "string" + } + }, + "After": { + "type": "array", + "items": { + "type": "string" + } + }, + "Before": { + "type": "array", + "items": { + "type": "string" + } + }, + "Requires": { + "type": "array", + "items": { + "type": "string" + } + }, + "ConditionPathExists": { + "type": "array", + "items": { + "type": "string" + } + }, + "ConditionPathIsDirectory": { + "type": "array", + "items": { + "type": "string" + } + }, + "DefaultDependencies": { + "type": "boolean" + } + } + }, + "Service": { + "additionalProperties": false, + "type": "object", + "description": "'Service' configuration section of a unit file.", + "properties": { + "Restart": { + "type": "string", + "enum": [ + "no", + "on-success", + "on-failure", + "on-abnormal", + "on-watchdog", + "on-abort", + "always" + ] + } + } + }, + "Container": { + "additionalProperties": false, + "type": "object", + "description": "'Container' configuration section of a unit file.", + "required": [ + "Image" + ], + "properties": { + "Environment": { + "type": "array", + "description": "Sets environment variables for executed process.", + "items": { + "type": "object", + "description": "Sets environment variables for executed process.", + "additionalProperties": false, + "properties": { + "key": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*" + }, + "value": { + "type": "string" + } + } + } + }, + "Image": { + "description": "Container Image to use", + "type": "string" + }, + "Exec": { + "description": "Command to execute in container", + "type": "string" + }, + "Volume": { + "description": "Volumes to use", + "type": "array", + "items": { + "type": "string" + } + }, + "User": { + "description": "Run as user", + "type": "string" + }, + "Group": { + "description": "Run as group", + "type": "string" + }, + "AddDevice": { + "description": "Add device to container", + "type": "string" + }, + "Network": { + "description": "What network option to use", + "type": "string" + }, + "WorkingDir": { + "description": "Working directory for initial process", + "type": "string" + } + } + }, + "Volume": { + "additionalProperties": false, + "type": "object", + "description": "'Volume' configuration section of a unit file.", + "required": [ + "What" + ], + "properties": { + "VolumeName": { + "description": "Override volume name", + "type": "string" + }, + "Driver": { + "description": "What volume driver to use", + "type": "string" + }, + "Image": { + "description": "Image to use if driver is image", + "type": "string" + }, + "User": { + "description": "User to use as owner of the volume", + "type": "string" + }, + "Group": { + "description": "Group to use as owner of the volume", + "type": "string" + } + } + }, + "Network": { + "additionalProperties": false, + "type": "object", + "description": "'Network' configuration section of a unit file.", + "properties": { + "Gateway": { + "description": "Addres of gaterway", + "type": "boolean" + }, + "DNS": { + "description": "Address of DNS server", + "type": "boolean" + }, + "IPRange": { + "description": "Range to allocate IPs from", + "type": "boolean" + }, + "Subnet": { + "description": "Subnet in CIDR notation", + "type": "boolean" + }, + "Driver": { + "description": "What network driver to use", + "type": "boolean" + }, + "NetworkName": { + "description": "Override network name", + "type": "boolean" + } + } + }, + "Install": { + "additionalProperties": false, + "type": "object", + "description": "'Install' configuration section of a unit file.", + "properties": { + "WantedBy": { + "type": "array", + "items": { + "type": "string" + } + }, + "RequiredBy": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } +}