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