Skip to content

Commit

Permalink
Add org.osbuild.containers.unit.create stage
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
alexlarsson committed Sep 17, 2024
1 parent a2f46cb commit 9a03607
Show file tree
Hide file tree
Showing 2 changed files with 391 additions and 0 deletions.
61 changes: 61 additions & 0 deletions stages/org.osbuild.containers.unit.create
Original file line number Diff line number Diff line change
@@ -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)
330 changes: 330 additions & 0 deletions stages/org.osbuild.containers.unit.create.meta.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
}
}
}
}

0 comments on commit 9a03607

Please sign in to comment.