diff --git a/stages/org.osbuild.systemd b/stages/org.osbuild.systemd index 6067f1fc..c82d331a 100755 --- a/stages/org.osbuild.systemd +++ b/stages/org.osbuild.systemd @@ -1,25 +1,38 @@ #!/usr/bin/python3 """ -Enable or disable systemd services +Configure Systemd services. -Enable or disable systemd units (service, socket, path, etc.) +Enable, disable or mask systemd units (service, socket, path, etc.) by running +`systemctl` from the buildhost. This stage runs `systemctl enable` for all `enabled_services` items, which may create symlinks under `/etc/systemd/system`. After enabling units, it runs `systemctl disable` for all `disabled_services` items, which will delete _all_ symlinks to the named services. -Uses `systemctl` from the buildhost. +The 'default_target' option allows to configure the default Systemd target. + +The 'unit_dropins' option allows to create Systemd unit drop-in configuration +files in `/usr/lib/systemd/system/.d/`. Its properties are names of +'.service' files to be modified using drop-ins. These names are validated using +the same rules as specified by systemd.unit(5) and they must contain the +'.service' suffix (other types of unit files are not supported). Value of each +specified '.service' file is an object, which properties are names of drop-in +configuration '.conf' files. Drop-in configuration files can currently specify +the following subset of options: + - 'Service' section + - 'Environment' option """ - +import os import subprocess import sys +import configparser import osbuild.api -SCHEMA = """ +SCHEMA = r""" "additionalProperties": false, "properties": { "enabled_services": { @@ -40,16 +53,73 @@ SCHEMA = """ "default_target": { "type": "string", "description": "The default target to boot into" + }, + "unit_dropins": { + "additionalProperties": false, + "type": "object", + "description": "Systemd unit drop-in configurations.", + "patternProperties": { + "^[\\w:.\\\\-]+[@]{0,1}[\\w:.\\\\-]*\\.service$": { + "additionalProperties": false, + "type": "object", + "description": "Drop-in configurations for a '.service' unit.", + "patternProperties": { + "^[\\w.-]{1,250}\\.conf$": { + "additionalProperties": false, + "type": "object", + "description": "Drop-in configuration for a '.service' unit.", + "properties": { + "Service": { + "additionalProperties": false, + "type": "object", + "description": "'Service' configuration section of a unit file.", + "properties": { + "Environment": { + "type": "string", + "description": "Sets environment variables for executed process." + } + } + } + } + } + } + } + } } } """ +def configure_unit_dropins(tree, unit_dropins_options): + for unit, unit_dropins in unit_dropins_options.items(): + # ensure the unit name + ".d" does not exceed maximum filename length + if len(unit+".d") > 255: + raise ValueError(f"Error: the {unit} unit drop-in directory exceeds the maximum filename length.") + + unit_dropins_dir = f"{tree}/usr/lib/systemd/system/{unit}.d" + os.makedirs(unit_dropins_dir, exist_ok=True) + + for dropin_file, dropin_config in unit_dropins.items(): + config = configparser.ConfigParser() + # prevent conversion of the option name to lowercase + config.optionxform = lambda option: option + + for section, options in dropin_config.items(): + if not config.has_section(section): + config.add_section(section) + for option, value in options.items(): + config.set(section, option, str(value)) + + with open(f"{unit_dropins_dir}/{dropin_file}", "w") as f: + config.write(f, space_around_delimiters=False) + + def main(tree, options): enabled_services = options.get("enabled_services", []) disabled_services = options.get("disabled_services", []) masked_services = options.get("masked_services", []) default_target = options.get("default_target") + unit_dropins_options = options.get("unit_dropins", {}) for service in enabled_services: subprocess.run(["systemctl", "--root", tree, "enable", service], check=True) @@ -63,6 +133,8 @@ def main(tree, options): if default_target: subprocess.run(["systemctl", "--root", tree, "set-default", default_target], check=True) + configure_unit_dropins(tree, unit_dropins_options) + return 0 diff --git a/test/data/stages/systemd/b.json b/test/data/stages/systemd/b.json index ae563ce7..1efc6fc7 100644 --- a/test/data/stages/systemd/b.json +++ b/test/data/stages/systemd/b.json @@ -456,7 +456,16 @@ ], "disabled_services": [ "sshd" - ] + ], + "unit_dropins": { + "nm-cloud-setup.service": { + "10-rh-enable-for-ec2.conf": { + "Service": { + "Environment": "NM_CLOUD_SETUP_EC2=yes" + } + } + } + } } } ] diff --git a/test/data/stages/systemd/b.mpp.json b/test/data/stages/systemd/b.mpp.json index f76de50f..c99ecadf 100644 --- a/test/data/stages/systemd/b.mpp.json +++ b/test/data/stages/systemd/b.mpp.json @@ -42,7 +42,16 @@ ], "disabled_services": [ "sshd" - ] + ], + "unit_dropins": { + "nm-cloud-setup.service": { + "10-rh-enable-for-ec2.conf": { + "Service": { + "Environment": "NM_CLOUD_SETUP_EC2=yes" + } + } + } + } } } ] diff --git a/test/data/stages/systemd/diff.json b/test/data/stages/systemd/diff.json index 7ceb776c..83a7b48e 100644 --- a/test/data/stages/systemd/diff.json +++ b/test/data/stages/systemd/diff.json @@ -1,7 +1,9 @@ { "added_files": [ "/etc/systemd/system/ldconfig.service", - "/etc/systemd/system/multi-user.target.wants/nftables.service" + "/etc/systemd/system/multi-user.target.wants/nftables.service", + "/usr/lib/systemd/system/nm-cloud-setup.service.d", + "/usr/lib/systemd/system/nm-cloud-setup.service.d/10-rh-enable-for-ec2.conf" ], "deleted_files": [ "/etc/systemd/system/multi-user.target.wants/sshd.service"