#!/usr/bin/python # coding: utf-8 -*- # 2022, Sébastien Gendre # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = ''' module: podman_generate_systemd author: - Sébastien Gendre (@CyberFox001) short_description: Generate systemd unit from a pod or a container description: - Generate systemd .service unit file(s) from a pod or a container - Support Ansible check mode options: name: description: - Name of the pod or container to export type: str required: true dest: description: - Destination of the generated systemd unit file(s). - Use C(/etc/systemd/system) for the system-wide systemd instance. - Use C(/etc/systemd/user) or C(~/.config/systemd/user) for use with per-user instances of systemd. type: path force: description: - Replace the systemd unit file(s) even if it already exists. - This works with dest option. type: bool default: false new: description: - Generate unit files that create containers and pods, not only start them. - Refer to podman-generate-systemd(1) man page for more information. type: bool default: false restart_policy: description: - Restart policy of the service type: str choices: - no-restart - on-success - on-failure - on-abnormal - on-watchdog - on-abort - always restart_sec: description: - Configures the time to sleep before restarting a service (as configured with restart-policy). - Takes a value in seconds. - Only with Podman 4.0.0 and above type: int start_timeout: description: - Override the default start timeout for the container with the given value in seconds. - Only with Podman 4.0.0 and above type: int stop_timeout: description: - Override the default stop timeout for the container with the given value in seconds. type: int env: description: - Set environment variables to the systemd unit files. - Keys are the environment variable names, and values are the environment variable values - Only with Podman 4.3.0 and above type: dict use_names: description: - Use name of the containers for the start, stop, and description in the unit file. type: bool default: true container_prefix: description: - Set the systemd unit name prefix for containers. - If not set, use the default defined by podman, C(container). - Refer to podman-generate-systemd(1) man page for more information. type: str pod_prefix: description: - Set the systemd unit name prefix for pods. - If not set, use the default defined by podman, C(pod). - Refer to podman-generate-systemd(1) man page for more information. type: str separator: description: - Systemd unit name separator between the name/id of a container/pod and the prefix. - If not set, use the default defined by podman, C(-). - Refer to podman-generate-systemd(1) man page for more information. type: str no_header: description: - Do not generate the header including meta data such as the Podman version and the timestamp. type: bool default: false after: description: - Add the systemd unit after (C(After=)) option, that ordering dependencies between the list of dependencies and this service. - This option may be specified more than once. - User-defined dependencies will be appended to the generated unit file - But any existing options such as needed or defined by default (e.g. C(online.target)) will not be removed or overridden. - Only with Podman 4.0.0 and above type: list elements: str wants: description: - Add the systemd unit wants (C(Wants=)) option, that this service is (weak) dependent on. - This option may be specified more than once. - This option does not influence the order in which services are started or stopped. - User-defined dependencies will be appended to the generated unit file - But any existing options such as needed or defined by default (e.g. C(online.target)) will not be removed or overridden. - Only with Podman 4.0.0 and above type: list elements: str requires: description: - Set the systemd unit requires (Requires=) option. - Similar to wants, but declares a stronger requirement dependency. - Only with Podman 4.0.0 and above type: list elements: str executable: description: - C(Podman) executable name or full path type: str default: podman requirements: - Podman installed on target host notes: - If you indicate a pod, the systemd units for it and all its containers will be generated - Create all your pods, containers and their dependencies before generating the systemd files - If a container or pod is already started before you do a C(systemctl daemon-reload), systemd will not see the container or pod as started - Stop your container or pod before you do a C(systemctl daemon-reload), then you can start them with C(systemctl start my_container.service) ''' EXAMPLES = ''' # Example of creating a container and systemd unit file. # When using podman_generate_systemd with new:true then # the container needs rm:true for idempotence. - name: Create postgres container containers.podman.podman_container: name: postgres image: docker.io/library/postgres:latest rm: true state: created - name: Generate systemd unit file for postgres container containers.podman.podman_generate_systemd: name: postgres new: true no_header: true dest: /etc/systemd/system - name: Ensure postgres container is started and enabled ansible.builtin.systemd: name: container-postgres daemon_reload: true state: started enabled: true # Example of creating a container and integrate it into systemd - name: A postgres container must exist, stopped containers.podman.podman_container: name: postgres_local image: docker.io/library/postgres:latest state: stopped - name: Systemd unit files for postgres container must exist containers.podman.podman_generate_systemd: name: postgres_local dest: ~/.config/systemd/user/ - name: Postgres container must be started and enabled on systemd ansible.builtin.systemd: name: container-postgres_local scope: user daemon_reload: true state: started enabled: true # Generate the unit files, but store them on an Ansible variable # instead of writing them on target host - name: Systemd unit files for postgres container must be generated containers.podman.podman_generate_systemd: name: postgres_local register: postgres_local_systemd_unit # Generate the unit files with environment variables sets - name: Systemd unit files for postgres container must be generated containers.podman.podman_generate_systemd: name: postgres_local env: POSTGRES_USER: my_app POSTGRES_PASSWORD: example register: postgres_local_systemd_unit ''' RETURN = ''' systemd_units: description: A copy of the generated systemd .service unit(s) returned: always type: dict sample: { "container-postgres_local": " #Content of the systemd .servec unit for postgres_local container", "pod-my_webapp": " #Content of the systemd .servec unit for my_webapp pod", } podman_command: description: A copy of the podman command used to generate the systemd unit(s) returned: always type: str sample: "podman generate systemd my_webapp" ''' import os from ansible.module_utils.basic import AnsibleModule import json from ansible_collections.containers.podman.plugins.module_utils.podman.common import compare_systemd_file_content RESTART_POLICY_CHOICES = [ 'no-restart', 'on-success', 'on-failure', 'on-abnormal', 'on-watchdog', 'on-abort', 'always', ] def generate_systemd(module): '''Generate systemd .service unit file from a pod or container. Parameter: - module (AnsibleModule): An AnsibleModule object Returns (tuple[bool, list[str], str]): - A boolean which indicate whether the targeted systemd state is modified - A copy of the generated systemd .service units content - A copy of the command, as a string ''' # Flag which indicate whether the targeted system state is modified changed = False # Build the podman command, based on the module parameters command_options = [] # New option if module.params['new']: command_options.append('--new') # Restart policy option restart_policy = module.params['restart_policy'] if restart_policy: # add the restart policy to options if restart_policy == 'no-restart': restart_policy = 'no' command_options.append( '--restart-policy={restart_policy}'.format( restart_policy=restart_policy, ), ) # Restart-sec option (only for Podman 4.0.0 and above) restart_sec = module.params['restart_sec'] if restart_sec: command_options.append( '--restart-sec={restart_sec}'.format( restart_sec=restart_sec, ), ) # Start-timeout option (only for Podman 4.0.0 and above) start_timeout = module.params['start_timeout'] if start_timeout: command_options.append( '--start-timeout={start_timeout}'.format( start_timeout=start_timeout, ), ) # Stop-timeout option stop_timeout = module.params['stop_timeout'] if stop_timeout: command_options.append( '--stop-timeout={stop_timeout}'.format( stop_timeout=stop_timeout, ), ) # Use container name(s) option if module.params['use_names']: command_options.append('--name') # Container-prefix option container_prefix = module.params['container_prefix'] if container_prefix is not None: command_options.append( '--container-prefix={container_prefix}'.format( container_prefix=container_prefix, ), ) # Pod-prefix option pod_prefix = module.params['pod_prefix'] if pod_prefix is not None: command_options.append( '--pod-prefix={pod_prefix}'.format( pod_prefix=pod_prefix, ), ) # Separator option separator = module.params['separator'] if separator is not None: command_options.append( '--separator={separator}'.format( separator=separator, ), ) # No-header option if module.params['no_header']: command_options.append('--no-header') # After option (only for Podman 4.0.0 and above) after = module.params['after'] if after: for item in after: command_options.append( '--after={item}'.format( item=item, ), ) # Wants option (only for Podman 4.0.0 and above) wants = module.params['wants'] if wants: for item in wants: command_options.append( '--wants={item}'.format( item=item, ) ) # Requires option (only for Podman 4.0.0 and above) requires = module.params['requires'] if requires: for item in requires: command_options.append( '--requires={item}'.format( item=item, ), ) # Environment variables (only for Podman 4.3.0 and above) environment_variables = module.params['env'] if environment_variables: for env_var_name, env_var_value in environment_variables.items(): command_options.append( "-e='{env_var_name}={env_var_value}'".format( env_var_name=env_var_name, env_var_value=env_var_value, ), ) # Set output format, of podman command, to json command_options.extend(['--format', 'json']) # Full command build, with option included # Base of the command command = [ module.params['executable'], 'generate', 'systemd', ] # Add the options to the commande command.extend(command_options) # Add pod or container name to the command command.append(module.params['name']) # Build the string version of the command, only for module return command_str = ' '.join(command) # Run the podman command to generated systemd .service unit(s) content return_code, stdout, stderr = module.run_command(command) # In case of error in running the command if return_code != 0: # Print information about the error and return and empty dictionary message = 'Error generating systemd .service unit(s).' message += ' Command executed: {command_str}' message += ' Command returned with code: {return_code}.' message += ' Error message: {stderr}.' module.fail_json( msg=message.format( command_str=command_str, return_code=return_code, stderr=stderr, ), changed=changed, systemd_units={}, podman_command=command_str, ) # In case of command execution success, its stdout is a json # dictionary. This dictionary is all the generated systemd units. # Each key value pair is one systemd unit. The key is the unit name # and the value is the unit content. # Load the returned json dictionary as a python dictionary systemd_units = json.loads(stdout) # Write the systemd .service unit(s) content to file(s), if # requested if module.params['dest']: try: systemd_units_dest = module.params['dest'] # If destination don't exist if not os.path.exists(systemd_units_dest): # If not in check mode, make it if not module.check_mode: os.makedirs(systemd_units_dest) changed = True # If destination exist but not a directory if not os.path.isdir(systemd_units_dest): # Stop and tell user that the destination is not a directory message = "Destination {systemd_units_dest} is not a directory." message += " Can't save systemd unit files in." module.fail_json( msg=message.format( systemd_units_dest=systemd_units_dest, ), changed=changed, systemd_units=systemd_units, podman_command=command_str, ) # Write each systemd unit, if needed for unit_name, unit_content in systemd_units.items(): # Build full path to unit file unit_file_name = unit_name + '.service' unit_file_full_path = os.path.join( systemd_units_dest, unit_file_name, ) if module.params['force']: # Force to replace the existing unit file need_to_write_file = True else: # See if we need to write the unit file, default yes need_to_write_file = bool(compare_systemd_file_content( unit_file_full_path, unit_content)) # Write the file, if needed if need_to_write_file: with open(unit_file_full_path, 'w') as unit_file: # If not in check mode, write the file if not module.check_mode: unit_file.write(unit_content) changed = True except Exception as exception: # When exception occurs while trying to write units file message = 'PODMAN-GENERATE-SYSTEMD-DEBUG: ' message += 'Error writing systemd units files: ' message += '{exception}' module.log( message.format( exception=exception ), ) # Return the systemd .service unit(s) content return changed, systemd_units, command_str def run_module(): '''Run the module on the target''' # Build the list of parameters user can use module_parameters = { 'name': { 'type': 'str', 'required': True, }, 'dest': { 'type': 'path', 'required': False, }, 'new': { 'type': 'bool', 'required': False, 'default': False, }, 'force': { 'type': 'bool', 'required': False, 'default': False, }, 'restart_policy': { 'type': 'str', 'required': False, 'choices': RESTART_POLICY_CHOICES, }, 'restart_sec': { 'type': 'int', 'required': False, }, 'start_timeout': { 'type': 'int', 'required': False, }, 'stop_timeout': { 'type': 'int', 'required': False, }, 'env': { 'type': 'dict', 'required': False, }, 'use_names': { 'type': 'bool', 'required': False, 'default': True, }, 'container_prefix': { 'type': 'str', 'required': False, }, 'pod_prefix': { 'type': 'str', 'required': False, }, 'separator': { 'type': 'str', 'required': False, }, 'no_header': { 'type': 'bool', 'required': False, 'default': False, }, 'after': { 'type': 'list', 'elements': 'str', 'required': False, }, 'wants': { 'type': 'list', 'elements': 'str', 'required': False, }, 'requires': { 'type': 'list', 'elements': 'str', 'required': False, }, 'executable': { 'type': 'str', 'required': False, 'default': 'podman', }, } # Build result dictionary result = { 'changed': False, 'systemd_units': {}, 'podman_command': '', } # Build the Ansible Module module = AnsibleModule( argument_spec=module_parameters, supports_check_mode=True ) # Generate the systemd units state_changed, systemd_units, podman_command = generate_systemd(module) result['changed'] = state_changed result['systemd_units'] = systemd_units result['podman_command'] = podman_command # Return the result module.exit_json(**result) def main(): '''Main function of this script.''' run_module() if __name__ == '__main__': main()