diff --git a/changelogs/fragments/systemd_services.yml b/changelogs/fragments/systemd_services.yml new file mode 100644 index 00000000000..eec0bcc3a14 --- /dev/null +++ b/changelogs/fragments/systemd_services.yml @@ -0,0 +1,2 @@ +minor_changes: + - systemd is now systemd_service to better reflect the scope of the module, systemd is kept as an alias for backwards compatibility. diff --git a/lib/ansible/modules/systemd.py b/lib/ansible/modules/systemd.py deleted file mode 100644 index 6fc606253eb..00000000000 --- a/lib/ansible/modules/systemd.py +++ /dev/null @@ -1,569 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright: (c) 2016, Brian Coca -# 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: systemd -author: - - Ansible Core Team -version_added: "2.2" -short_description: Manage systemd units -description: - - Controls systemd units (services, timers, and so on) on remote hosts. -options: - name: - description: - - Name of the unit. This parameter takes the name of exactly one unit to work with. - - When no extension is given, it is implied to a C(.service) as systemd. - - When using in a chroot environment you always need to specify the name of the unit with the extension. For example, C(crond.service). - type: str - aliases: [ service, unit ] - state: - description: - - C(started)/C(stopped) are idempotent actions that will not run commands unless necessary. - C(restarted) will always bounce the unit. C(reloaded) will always reload. - type: str - choices: [ reloaded, restarted, started, stopped ] - enabled: - description: - - Whether the unit should start on boot. B(At least one of state and enabled are required.) - type: bool - force: - description: - - Whether to override existing symlinks. - type: bool - version_added: 2.6 - masked: - description: - - Whether the unit should be masked or not, a masked unit is impossible to start. - type: bool - daemon_reload: - description: - - Run daemon-reload before doing any other operations, to make sure systemd has read any changes. - - When set to C(true), runs daemon-reload even if the module does not start or stop anything. - type: bool - default: no - aliases: [ daemon-reload ] - daemon_reexec: - description: - - Run daemon_reexec command before doing any other operations, the systemd manager will serialize the manager state. - type: bool - default: no - aliases: [ daemon-reexec ] - version_added: "2.8" - scope: - description: - - Run systemctl within a given service manager scope, either as the default system scope C(system), - the current user's scope C(user), or the scope of all users C(global). - - "For systemd to work with 'user', the executing user must have its own instance of dbus started and accessible (systemd requirement)." - - "The user dbus process is normally started during normal login, but not during the run of Ansible tasks. - Otherwise you will probably get a 'Failed to connect to bus: no such file or directory' error." - - The user must have access, normally given via setting the C(XDG_RUNTIME_DIR) variable, see example below. - - type: str - choices: [ system, user, global ] - default: system - version_added: "2.7" - no_block: - description: - - Do not synchronously wait for the requested operation to finish. - Enqueued job will continue without Ansible blocking on its completion. - type: bool - default: no - version_added: "2.3" -extends_documentation_fragment: action_common_attributes -attributes: - check_mode: - support: full - diff_mode: - support: none - platform: - platforms: posix -notes: - - Since 2.4, one of the following options is required C(state), C(enabled), C(masked), C(daemon_reload), (C(daemon_reexec) since 2.8), - and all except C(daemon_reload) and (C(daemon_reexec) since 2.8) also require C(name). - - Before 2.4 you always required C(name). - - Globs are not supported in name, i.e C(postgres*.service). - - The service names might vary by specific OS/distribution -requirements: - - A system managed by systemd. -''' - -EXAMPLES = ''' -- name: Make sure a service unit is running - ansible.builtin.systemd: - state: started - name: httpd - -- name: Stop service cron on debian, if running - ansible.builtin.systemd: - name: cron - state: stopped - -- name: Restart service cron on centos, in all cases, also issue daemon-reload to pick up config changes - ansible.builtin.systemd: - state: restarted - daemon_reload: yes - name: crond - -- name: Reload service httpd, in all cases - ansible.builtin.systemd: - name: httpd.service - state: reloaded - -- name: Enable service httpd and ensure it is not masked - ansible.builtin.systemd: - name: httpd - enabled: yes - masked: no - -- name: Enable a timer unit for dnf-automatic - ansible.builtin.systemd: - name: dnf-automatic.timer - state: started - enabled: yes - -- name: Just force systemd to reread configs (2.4 and above) - ansible.builtin.systemd: - daemon_reload: yes - -- name: Just force systemd to re-execute itself (2.8 and above) - ansible.builtin.systemd: - daemon_reexec: yes - -- name: Run a user service when XDG_RUNTIME_DIR is not set on remote login - ansible.builtin.systemd: - name: myservice - state: started - scope: user - environment: - XDG_RUNTIME_DIR: "/run/user/{{ myuid }}" -''' - -RETURN = ''' -status: - description: A dictionary with the key=value pairs returned from C(systemctl show). - returned: success - type: complex - sample: { - "ActiveEnterTimestamp": "Sun 2016-05-15 18:28:49 EDT", - "ActiveEnterTimestampMonotonic": "8135942", - "ActiveExitTimestampMonotonic": "0", - "ActiveState": "active", - "After": "auditd.service systemd-user-sessions.service time-sync.target systemd-journald.socket basic.target system.slice", - "AllowIsolate": "no", - "Before": "shutdown.target multi-user.target", - "BlockIOAccounting": "no", - "BlockIOWeight": "1000", - "CPUAccounting": "no", - "CPUSchedulingPolicy": "0", - "CPUSchedulingPriority": "0", - "CPUSchedulingResetOnFork": "no", - "CPUShares": "1024", - "CanIsolate": "no", - "CanReload": "yes", - "CanStart": "yes", - "CanStop": "yes", - "CapabilityBoundingSet": "18446744073709551615", - "ConditionResult": "yes", - "ConditionTimestamp": "Sun 2016-05-15 18:28:49 EDT", - "ConditionTimestampMonotonic": "7902742", - "Conflicts": "shutdown.target", - "ControlGroup": "/system.slice/crond.service", - "ControlPID": "0", - "DefaultDependencies": "yes", - "Delegate": "no", - "Description": "Command Scheduler", - "DevicePolicy": "auto", - "EnvironmentFile": "/etc/sysconfig/crond (ignore_errors=no)", - "ExecMainCode": "0", - "ExecMainExitTimestampMonotonic": "0", - "ExecMainPID": "595", - "ExecMainStartTimestamp": "Sun 2016-05-15 18:28:49 EDT", - "ExecMainStartTimestampMonotonic": "8134990", - "ExecMainStatus": "0", - "ExecReload": "{ path=/bin/kill ; argv[]=/bin/kill -HUP $MAINPID ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }", - "ExecStart": "{ path=/usr/sbin/crond ; argv[]=/usr/sbin/crond -n $CRONDARGS ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }", - "FragmentPath": "/usr/lib/systemd/system/crond.service", - "GuessMainPID": "yes", - "IOScheduling": "0", - "Id": "crond.service", - "IgnoreOnIsolate": "no", - "IgnoreOnSnapshot": "no", - "IgnoreSIGPIPE": "yes", - "InactiveEnterTimestampMonotonic": "0", - "InactiveExitTimestamp": "Sun 2016-05-15 18:28:49 EDT", - "InactiveExitTimestampMonotonic": "8135942", - "JobTimeoutUSec": "0", - "KillMode": "process", - "KillSignal": "15", - "LimitAS": "18446744073709551615", - "LimitCORE": "18446744073709551615", - "LimitCPU": "18446744073709551615", - "LimitDATA": "18446744073709551615", - "LimitFSIZE": "18446744073709551615", - "LimitLOCKS": "18446744073709551615", - "LimitMEMLOCK": "65536", - "LimitMSGQUEUE": "819200", - "LimitNICE": "0", - "LimitNOFILE": "4096", - "LimitNPROC": "3902", - "LimitRSS": "18446744073709551615", - "LimitRTPRIO": "0", - "LimitRTTIME": "18446744073709551615", - "LimitSIGPENDING": "3902", - "LimitSTACK": "18446744073709551615", - "LoadState": "loaded", - "MainPID": "595", - "MemoryAccounting": "no", - "MemoryLimit": "18446744073709551615", - "MountFlags": "0", - "Names": "crond.service", - "NeedDaemonReload": "no", - "Nice": "0", - "NoNewPrivileges": "no", - "NonBlocking": "no", - "NotifyAccess": "none", - "OOMScoreAdjust": "0", - "OnFailureIsolate": "no", - "PermissionsStartOnly": "no", - "PrivateNetwork": "no", - "PrivateTmp": "no", - "RefuseManualStart": "no", - "RefuseManualStop": "no", - "RemainAfterExit": "no", - "Requires": "basic.target", - "Restart": "no", - "RestartUSec": "100ms", - "Result": "success", - "RootDirectoryStartOnly": "no", - "SameProcessGroup": "no", - "SecureBits": "0", - "SendSIGHUP": "no", - "SendSIGKILL": "yes", - "Slice": "system.slice", - "StandardError": "inherit", - "StandardInput": "null", - "StandardOutput": "journal", - "StartLimitAction": "none", - "StartLimitBurst": "5", - "StartLimitInterval": "10000000", - "StatusErrno": "0", - "StopWhenUnneeded": "no", - "SubState": "running", - "SyslogLevelPrefix": "yes", - "SyslogPriority": "30", - "TTYReset": "no", - "TTYVHangup": "no", - "TTYVTDisallocate": "no", - "TimeoutStartUSec": "1min 30s", - "TimeoutStopUSec": "1min 30s", - "TimerSlackNSec": "50000", - "Transient": "no", - "Type": "simple", - "UMask": "0022", - "UnitFileState": "enabled", - "WantedBy": "multi-user.target", - "Wants": "system.slice", - "WatchdogTimestampMonotonic": "0", - "WatchdogUSec": "0", - } -''' # NOQA - -import os - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.facts.system.chroot import is_chroot -from ansible.module_utils.service import sysv_exists, sysv_is_enabled, fail_if_missing -from ansible.module_utils._text import to_native - - -def is_running_service(service_status): - return service_status['ActiveState'] in set(['active', 'activating']) - - -def is_deactivating_service(service_status): - return service_status['ActiveState'] in set(['deactivating']) - - -def request_was_ignored(out): - return '=' not in out and ('ignoring request' in out or 'ignoring command' in out) - - -def parse_systemctl_show(lines): - # The output of 'systemctl show' can contain values that span multiple lines. At first glance it - # appears that such values are always surrounded by {}, so the previous version of this code - # assumed that any value starting with { was a multi-line value; it would then consume lines - # until it saw a line that ended with }. However, it is possible to have a single-line value - # that starts with { but does not end with } (this could happen in the value for Description=, - # for example), and the previous version of this code would then consume all remaining lines as - # part of that value. Cryptically, this would lead to Ansible reporting that the service file - # couldn't be found. - # - # To avoid this issue, the following code only accepts multi-line values for keys whose names - # start with Exec (e.g., ExecStart=), since these are the only keys whose values are known to - # span multiple lines. - parsed = {} - multival = [] - k = None - for line in lines: - if k is None: - if '=' in line: - k, v = line.split('=', 1) - if k.startswith('Exec') and v.lstrip().startswith('{'): - if not v.rstrip().endswith('}'): - multival.append(v) - continue - parsed[k] = v.strip() - k = None - else: - multival.append(line) - if line.rstrip().endswith('}'): - parsed[k] = '\n'.join(multival).strip() - multival = [] - k = None - return parsed - - -# =========================================== -# Main control flow - -def main(): - # initialize - module = AnsibleModule( - argument_spec=dict( - name=dict(type='str', aliases=['service', 'unit']), - state=dict(type='str', choices=['reloaded', 'restarted', 'started', 'stopped']), - enabled=dict(type='bool'), - force=dict(type='bool'), - masked=dict(type='bool'), - daemon_reload=dict(type='bool', default=False, aliases=['daemon-reload']), - daemon_reexec=dict(type='bool', default=False, aliases=['daemon-reexec']), - scope=dict(type='str', default='system', choices=['system', 'user', 'global']), - no_block=dict(type='bool', default=False), - ), - supports_check_mode=True, - required_one_of=[['state', 'enabled', 'masked', 'daemon_reload', 'daemon_reexec']], - required_by=dict( - state=('name', ), - enabled=('name', ), - masked=('name', ), - ), - ) - - unit = module.params['name'] - if unit is not None: - for globpattern in (r"*", r"?", r"["): - if globpattern in unit: - module.fail_json(msg="This module does not currently support using glob patterns, found '%s' in service name: %s" % (globpattern, unit)) - - systemctl = module.get_bin_path('systemctl', True) - - if os.getenv('XDG_RUNTIME_DIR') is None: - os.environ['XDG_RUNTIME_DIR'] = '/run/user/%s' % os.geteuid() - - ''' Set CLI options depending on params ''' - # if scope is 'system' or None, we can ignore as there is no extra switch. - # The other choices match the corresponding switch - if module.params['scope'] != 'system': - systemctl += " --%s" % module.params['scope'] - - if module.params['no_block']: - systemctl += " --no-block" - - if module.params['force']: - systemctl += " --force" - - rc = 0 - out = err = '' - result = dict( - name=unit, - changed=False, - status=dict(), - ) - - # Run daemon-reload first, if requested - if module.params['daemon_reload'] and not module.check_mode: - (rc, out, err) = module.run_command("%s daemon-reload" % (systemctl)) - if rc != 0: - module.fail_json(msg='failure %d during daemon-reload: %s' % (rc, err)) - - # Run daemon-reexec - if module.params['daemon_reexec'] and not module.check_mode: - (rc, out, err) = module.run_command("%s daemon-reexec" % (systemctl)) - if rc != 0: - module.fail_json(msg='failure %d during daemon-reexec: %s' % (rc, err)) - - if unit: - found = False - is_initd = sysv_exists(unit) - is_systemd = False - - # check service data, cannot error out on rc as it changes across versions, assume not found - (rc, out, err) = module.run_command("%s show '%s'" % (systemctl, unit)) - - if rc == 0 and not (request_was_ignored(out) or request_was_ignored(err)): - # load return of systemctl show into dictionary for easy access and return - if out: - result['status'] = parse_systemctl_show(to_native(out).split('\n')) - - is_systemd = 'LoadState' in result['status'] and result['status']['LoadState'] != 'not-found' - - is_masked = 'LoadState' in result['status'] and result['status']['LoadState'] == 'masked' - - # Check for loading error - if is_systemd and not is_masked and 'LoadError' in result['status']: - module.fail_json(msg="Error loading unit file '%s': %s" % (unit, result['status']['LoadError'])) - - # Workaround for https://github.com/ansible/ansible/issues/71528 - elif err and rc == 1 and 'Failed to parse bus message' in err: - result['status'] = parse_systemctl_show(to_native(out).split('\n')) - - unit_base, sep, suffix = unit.partition('@') - unit_search = '{unit_base}{sep}'.format(unit_base=unit_base, sep=sep) - (rc, out, err) = module.run_command("{systemctl} list-unit-files '{unit_search}*'".format(systemctl=systemctl, unit_search=unit_search)) - is_systemd = unit_search in out - - (rc, out, err) = module.run_command("{systemctl} is-active '{unit}'".format(systemctl=systemctl, unit=unit)) - result['status']['ActiveState'] = out.rstrip('\n') - - else: - # list taken from man systemctl(1) for systemd 244 - valid_enabled_states = [ - "enabled", - "enabled-runtime", - "linked", - "linked-runtime", - "masked", - "masked-runtime", - "static", - "indirect", - "disabled", - "generated", - "transient"] - - (rc, out, err) = module.run_command("%s is-enabled '%s'" % (systemctl, unit)) - if out.strip() in valid_enabled_states: - is_systemd = True - else: - # fallback list-unit-files as show does not work on some systems (chroot) - # not used as primary as it skips some services (like those using init.d) and requires .service/etc notation - (rc, out, err) = module.run_command("%s list-unit-files '%s'" % (systemctl, unit)) - if rc == 0: - is_systemd = True - else: - # Check for systemctl command - module.run_command(systemctl, check_rc=True) - - # Does service exist? - found = is_systemd or is_initd - if is_initd and not is_systemd: - module.warn('The service (%s) is actually an init script but the system is managed by systemd' % unit) - - # mask/unmask the service, if requested, can operate on services before they are installed - if module.params['masked'] is not None: - # state is not masked unless systemd affirms otherwise - (rc, out, err) = module.run_command("%s is-enabled '%s'" % (systemctl, unit)) - masked = out.strip() == "masked" - - if masked != module.params['masked']: - result['changed'] = True - if module.params['masked']: - action = 'mask' - else: - action = 'unmask' - - if not module.check_mode: - (rc, out, err) = module.run_command("%s %s '%s'" % (systemctl, action, unit)) - if rc != 0: - # some versions of system CAN mask/unmask non existing services, we only fail on missing if they don't - fail_if_missing(module, found, unit, msg='host') - - # Enable/disable service startup at boot if requested - if module.params['enabled'] is not None: - - if module.params['enabled']: - action = 'enable' - else: - action = 'disable' - - fail_if_missing(module, found, unit, msg='host') - - # do we need to enable the service? - enabled = False - (rc, out, err) = module.run_command("%s is-enabled '%s' -l" % (systemctl, unit)) - - # check systemctl result or if it is a init script - if rc == 0: - enabled = True - # Check if the service is indirect or alias and if out contains exactly 1 line of string 'indirect'/ 'alias' it's disabled - if out.splitlines() == ["indirect"] or out.splitlines() == ["alias"]: - enabled = False - - elif rc == 1: - # if not a user or global user service and both init script and unit file exist stdout should have enabled/disabled, otherwise use rc entries - if module.params['scope'] == 'system' and \ - is_initd and \ - not out.strip().endswith('disabled') and \ - sysv_is_enabled(unit): - enabled = True - - # default to current state - result['enabled'] = enabled - - # Change enable/disable if needed - if enabled != module.params['enabled']: - result['changed'] = True - if not module.check_mode: - (rc, out, err) = module.run_command("%s %s '%s'" % (systemctl, action, unit)) - if rc != 0: - module.fail_json(msg="Unable to %s service %s: %s" % (action, unit, out + err)) - - result['enabled'] = not enabled - - # set service state if requested - if module.params['state'] is not None: - fail_if_missing(module, found, unit, msg="host") - - # default to desired state - result['state'] = module.params['state'] - - # What is current service state? - if 'ActiveState' in result['status']: - action = None - if module.params['state'] == 'started': - if not is_running_service(result['status']): - action = 'start' - elif module.params['state'] == 'stopped': - if is_running_service(result['status']) or is_deactivating_service(result['status']): - action = 'stop' - else: - if not is_running_service(result['status']): - action = 'start' - else: - action = module.params['state'][:-2] # remove 'ed' from restarted/reloaded - result['state'] = 'started' - - if action: - result['changed'] = True - if not module.check_mode: - (rc, out, err) = module.run_command("%s %s '%s'" % (systemctl, action, unit)) - if rc != 0: - module.fail_json(msg="Unable to %s service %s: %s" % (action, unit, err)) - # check for chroot - elif is_chroot(module) or os.environ.get('SYSTEMD_OFFLINE') == '1': - module.warn("Target is a chroot or systemd is offline. This can lead to false positives or prevent the init system tools from working.") - else: - # this should not happen? - module.fail_json(msg="Service is in unknown state", status=result['status']) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/systemd.py b/lib/ansible/modules/systemd.py new file mode 120000 index 00000000000..75b70642bb8 --- /dev/null +++ b/lib/ansible/modules/systemd.py @@ -0,0 +1 @@ +systemd_service.py \ No newline at end of file diff --git a/lib/ansible/modules/systemd_service.py b/lib/ansible/modules/systemd_service.py new file mode 100644 index 00000000000..4cd323b9b55 --- /dev/null +++ b/lib/ansible/modules/systemd_service.py @@ -0,0 +1,569 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016, Brian Coca +# 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: systemd_service +author: + - Ansible Core Team +version_added: "2.2" +short_description: Manage systemd units +description: + - Controls systemd units (services, timers, and so on) on remote hosts. +options: + name: + description: + - Name of the unit. This parameter takes the name of exactly one unit to work with. + - When no extension is given, it is implied to a C(.service) as systemd. + - When using in a chroot environment you always need to specify the name of the unit with the extension. For example, C(crond.service). + type: str + aliases: [ service, unit ] + state: + description: + - C(started)/C(stopped) are idempotent actions that will not run commands unless necessary. + C(restarted) will always bounce the unit. C(reloaded) will always reload. + type: str + choices: [ reloaded, restarted, started, stopped ] + enabled: + description: + - Whether the unit should start on boot. B(At least one of state and enabled are required.) + type: bool + force: + description: + - Whether to override existing symlinks. + type: bool + version_added: 2.6 + masked: + description: + - Whether the unit should be masked or not, a masked unit is impossible to start. + type: bool + daemon_reload: + description: + - Run daemon-reload before doing any other operations, to make sure systemd has read any changes. + - When set to C(true), runs daemon-reload even if the module does not start or stop anything. + type: bool + default: no + aliases: [ daemon-reload ] + daemon_reexec: + description: + - Run daemon_reexec command before doing any other operations, the systemd manager will serialize the manager state. + type: bool + default: no + aliases: [ daemon-reexec ] + version_added: "2.8" + scope: + description: + - Run systemctl within a given service manager scope, either as the default system scope C(system), + the current user's scope C(user), or the scope of all users C(global). + - "For systemd to work with 'user', the executing user must have its own instance of dbus started and accessible (systemd requirement)." + - "The user dbus process is normally started during normal login, but not during the run of Ansible tasks. + Otherwise you will probably get a 'Failed to connect to bus: no such file or directory' error." + - The user must have access, normally given via setting the C(XDG_RUNTIME_DIR) variable, see example below. + + type: str + choices: [ system, user, global ] + default: system + version_added: "2.7" + no_block: + description: + - Do not synchronously wait for the requested operation to finish. + Enqueued job will continue without Ansible blocking on its completion. + type: bool + default: no + version_added: "2.3" +extends_documentation_fragment: action_common_attributes +attributes: + check_mode: + support: full + diff_mode: + support: none + platform: + platforms: posix +notes: + - Since 2.4, one of the following options is required C(state), C(enabled), C(masked), C(daemon_reload), (C(daemon_reexec) since 2.8), + and all except C(daemon_reload) and (C(daemon_reexec) since 2.8) also require C(name). + - Before 2.4 you always required C(name). + - Globs are not supported in name, i.e C(postgres*.service). + - The service names might vary by specific OS/distribution +requirements: + - A system managed by systemd. +''' + +EXAMPLES = ''' +- name: Make sure a service unit is running + ansible.builtin.systemd: + state: started + name: httpd + +- name: Stop service cron on debian, if running + ansible.builtin.systemd: + name: cron + state: stopped + +- name: Restart service cron on centos, in all cases, also issue daemon-reload to pick up config changes + ansible.builtin.systemd: + state: restarted + daemon_reload: yes + name: crond + +- name: Reload service httpd, in all cases + ansible.builtin.systemd: + name: httpd.service + state: reloaded + +- name: Enable service httpd and ensure it is not masked + ansible.builtin.systemd: + name: httpd + enabled: yes + masked: no + +- name: Enable a timer unit for dnf-automatic + ansible.builtin.systemd: + name: dnf-automatic.timer + state: started + enabled: yes + +- name: Just force systemd to reread configs (2.4 and above) + ansible.builtin.systemd: + daemon_reload: yes + +- name: Just force systemd to re-execute itself (2.8 and above) + ansible.builtin.systemd: + daemon_reexec: yes + +- name: Run a user service when XDG_RUNTIME_DIR is not set on remote login + ansible.builtin.systemd: + name: myservice + state: started + scope: user + environment: + XDG_RUNTIME_DIR: "/run/user/{{ myuid }}" +''' + +RETURN = ''' +status: + description: A dictionary with the key=value pairs returned from C(systemctl show). + returned: success + type: complex + sample: { + "ActiveEnterTimestamp": "Sun 2016-05-15 18:28:49 EDT", + "ActiveEnterTimestampMonotonic": "8135942", + "ActiveExitTimestampMonotonic": "0", + "ActiveState": "active", + "After": "auditd.service systemd-user-sessions.service time-sync.target systemd-journald.socket basic.target system.slice", + "AllowIsolate": "no", + "Before": "shutdown.target multi-user.target", + "BlockIOAccounting": "no", + "BlockIOWeight": "1000", + "CPUAccounting": "no", + "CPUSchedulingPolicy": "0", + "CPUSchedulingPriority": "0", + "CPUSchedulingResetOnFork": "no", + "CPUShares": "1024", + "CanIsolate": "no", + "CanReload": "yes", + "CanStart": "yes", + "CanStop": "yes", + "CapabilityBoundingSet": "18446744073709551615", + "ConditionResult": "yes", + "ConditionTimestamp": "Sun 2016-05-15 18:28:49 EDT", + "ConditionTimestampMonotonic": "7902742", + "Conflicts": "shutdown.target", + "ControlGroup": "/system.slice/crond.service", + "ControlPID": "0", + "DefaultDependencies": "yes", + "Delegate": "no", + "Description": "Command Scheduler", + "DevicePolicy": "auto", + "EnvironmentFile": "/etc/sysconfig/crond (ignore_errors=no)", + "ExecMainCode": "0", + "ExecMainExitTimestampMonotonic": "0", + "ExecMainPID": "595", + "ExecMainStartTimestamp": "Sun 2016-05-15 18:28:49 EDT", + "ExecMainStartTimestampMonotonic": "8134990", + "ExecMainStatus": "0", + "ExecReload": "{ path=/bin/kill ; argv[]=/bin/kill -HUP $MAINPID ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }", + "ExecStart": "{ path=/usr/sbin/crond ; argv[]=/usr/sbin/crond -n $CRONDARGS ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }", + "FragmentPath": "/usr/lib/systemd/system/crond.service", + "GuessMainPID": "yes", + "IOScheduling": "0", + "Id": "crond.service", + "IgnoreOnIsolate": "no", + "IgnoreOnSnapshot": "no", + "IgnoreSIGPIPE": "yes", + "InactiveEnterTimestampMonotonic": "0", + "InactiveExitTimestamp": "Sun 2016-05-15 18:28:49 EDT", + "InactiveExitTimestampMonotonic": "8135942", + "JobTimeoutUSec": "0", + "KillMode": "process", + "KillSignal": "15", + "LimitAS": "18446744073709551615", + "LimitCORE": "18446744073709551615", + "LimitCPU": "18446744073709551615", + "LimitDATA": "18446744073709551615", + "LimitFSIZE": "18446744073709551615", + "LimitLOCKS": "18446744073709551615", + "LimitMEMLOCK": "65536", + "LimitMSGQUEUE": "819200", + "LimitNICE": "0", + "LimitNOFILE": "4096", + "LimitNPROC": "3902", + "LimitRSS": "18446744073709551615", + "LimitRTPRIO": "0", + "LimitRTTIME": "18446744073709551615", + "LimitSIGPENDING": "3902", + "LimitSTACK": "18446744073709551615", + "LoadState": "loaded", + "MainPID": "595", + "MemoryAccounting": "no", + "MemoryLimit": "18446744073709551615", + "MountFlags": "0", + "Names": "crond.service", + "NeedDaemonReload": "no", + "Nice": "0", + "NoNewPrivileges": "no", + "NonBlocking": "no", + "NotifyAccess": "none", + "OOMScoreAdjust": "0", + "OnFailureIsolate": "no", + "PermissionsStartOnly": "no", + "PrivateNetwork": "no", + "PrivateTmp": "no", + "RefuseManualStart": "no", + "RefuseManualStop": "no", + "RemainAfterExit": "no", + "Requires": "basic.target", + "Restart": "no", + "RestartUSec": "100ms", + "Result": "success", + "RootDirectoryStartOnly": "no", + "SameProcessGroup": "no", + "SecureBits": "0", + "SendSIGHUP": "no", + "SendSIGKILL": "yes", + "Slice": "system.slice", + "StandardError": "inherit", + "StandardInput": "null", + "StandardOutput": "journal", + "StartLimitAction": "none", + "StartLimitBurst": "5", + "StartLimitInterval": "10000000", + "StatusErrno": "0", + "StopWhenUnneeded": "no", + "SubState": "running", + "SyslogLevelPrefix": "yes", + "SyslogPriority": "30", + "TTYReset": "no", + "TTYVHangup": "no", + "TTYVTDisallocate": "no", + "TimeoutStartUSec": "1min 30s", + "TimeoutStopUSec": "1min 30s", + "TimerSlackNSec": "50000", + "Transient": "no", + "Type": "simple", + "UMask": "0022", + "UnitFileState": "enabled", + "WantedBy": "multi-user.target", + "Wants": "system.slice", + "WatchdogTimestampMonotonic": "0", + "WatchdogUSec": "0", + } +''' # NOQA + +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.facts.system.chroot import is_chroot +from ansible.module_utils.service import sysv_exists, sysv_is_enabled, fail_if_missing +from ansible.module_utils._text import to_native + + +def is_running_service(service_status): + return service_status['ActiveState'] in set(['active', 'activating']) + + +def is_deactivating_service(service_status): + return service_status['ActiveState'] in set(['deactivating']) + + +def request_was_ignored(out): + return '=' not in out and ('ignoring request' in out or 'ignoring command' in out) + + +def parse_systemctl_show(lines): + # The output of 'systemctl show' can contain values that span multiple lines. At first glance it + # appears that such values are always surrounded by {}, so the previous version of this code + # assumed that any value starting with { was a multi-line value; it would then consume lines + # until it saw a line that ended with }. However, it is possible to have a single-line value + # that starts with { but does not end with } (this could happen in the value for Description=, + # for example), and the previous version of this code would then consume all remaining lines as + # part of that value. Cryptically, this would lead to Ansible reporting that the service file + # couldn't be found. + # + # To avoid this issue, the following code only accepts multi-line values for keys whose names + # start with Exec (e.g., ExecStart=), since these are the only keys whose values are known to + # span multiple lines. + parsed = {} + multival = [] + k = None + for line in lines: + if k is None: + if '=' in line: + k, v = line.split('=', 1) + if k.startswith('Exec') and v.lstrip().startswith('{'): + if not v.rstrip().endswith('}'): + multival.append(v) + continue + parsed[k] = v.strip() + k = None + else: + multival.append(line) + if line.rstrip().endswith('}'): + parsed[k] = '\n'.join(multival).strip() + multival = [] + k = None + return parsed + + +# =========================================== +# Main control flow + +def main(): + # initialize + module = AnsibleModule( + argument_spec=dict( + name=dict(type='str', aliases=['service', 'unit']), + state=dict(type='str', choices=['reloaded', 'restarted', 'started', 'stopped']), + enabled=dict(type='bool'), + force=dict(type='bool'), + masked=dict(type='bool'), + daemon_reload=dict(type='bool', default=False, aliases=['daemon-reload']), + daemon_reexec=dict(type='bool', default=False, aliases=['daemon-reexec']), + scope=dict(type='str', default='system', choices=['system', 'user', 'global']), + no_block=dict(type='bool', default=False), + ), + supports_check_mode=True, + required_one_of=[['state', 'enabled', 'masked', 'daemon_reload', 'daemon_reexec']], + required_by=dict( + state=('name', ), + enabled=('name', ), + masked=('name', ), + ), + ) + + unit = module.params['name'] + if unit is not None: + for globpattern in (r"*", r"?", r"["): + if globpattern in unit: + module.fail_json(msg="This module does not currently support using glob patterns, found '%s' in service name: %s" % (globpattern, unit)) + + systemctl = module.get_bin_path('systemctl', True) + + if os.getenv('XDG_RUNTIME_DIR') is None: + os.environ['XDG_RUNTIME_DIR'] = '/run/user/%s' % os.geteuid() + + ''' Set CLI options depending on params ''' + # if scope is 'system' or None, we can ignore as there is no extra switch. + # The other choices match the corresponding switch + if module.params['scope'] != 'system': + systemctl += " --%s" % module.params['scope'] + + if module.params['no_block']: + systemctl += " --no-block" + + if module.params['force']: + systemctl += " --force" + + rc = 0 + out = err = '' + result = dict( + name=unit, + changed=False, + status=dict(), + ) + + # Run daemon-reload first, if requested + if module.params['daemon_reload'] and not module.check_mode: + (rc, out, err) = module.run_command("%s daemon-reload" % (systemctl)) + if rc != 0: + module.fail_json(msg='failure %d during daemon-reload: %s' % (rc, err)) + + # Run daemon-reexec + if module.params['daemon_reexec'] and not module.check_mode: + (rc, out, err) = module.run_command("%s daemon-reexec" % (systemctl)) + if rc != 0: + module.fail_json(msg='failure %d during daemon-reexec: %s' % (rc, err)) + + if unit: + found = False + is_initd = sysv_exists(unit) + is_systemd = False + + # check service data, cannot error out on rc as it changes across versions, assume not found + (rc, out, err) = module.run_command("%s show '%s'" % (systemctl, unit)) + + if rc == 0 and not (request_was_ignored(out) or request_was_ignored(err)): + # load return of systemctl show into dictionary for easy access and return + if out: + result['status'] = parse_systemctl_show(to_native(out).split('\n')) + + is_systemd = 'LoadState' in result['status'] and result['status']['LoadState'] != 'not-found' + + is_masked = 'LoadState' in result['status'] and result['status']['LoadState'] == 'masked' + + # Check for loading error + if is_systemd and not is_masked and 'LoadError' in result['status']: + module.fail_json(msg="Error loading unit file '%s': %s" % (unit, result['status']['LoadError'])) + + # Workaround for https://github.com/ansible/ansible/issues/71528 + elif err and rc == 1 and 'Failed to parse bus message' in err: + result['status'] = parse_systemctl_show(to_native(out).split('\n')) + + unit_base, sep, suffix = unit.partition('@') + unit_search = '{unit_base}{sep}'.format(unit_base=unit_base, sep=sep) + (rc, out, err) = module.run_command("{systemctl} list-unit-files '{unit_search}*'".format(systemctl=systemctl, unit_search=unit_search)) + is_systemd = unit_search in out + + (rc, out, err) = module.run_command("{systemctl} is-active '{unit}'".format(systemctl=systemctl, unit=unit)) + result['status']['ActiveState'] = out.rstrip('\n') + + else: + # list taken from man systemctl(1) for systemd 244 + valid_enabled_states = [ + "enabled", + "enabled-runtime", + "linked", + "linked-runtime", + "masked", + "masked-runtime", + "static", + "indirect", + "disabled", + "generated", + "transient"] + + (rc, out, err) = module.run_command("%s is-enabled '%s'" % (systemctl, unit)) + if out.strip() in valid_enabled_states: + is_systemd = True + else: + # fallback list-unit-files as show does not work on some systems (chroot) + # not used as primary as it skips some services (like those using init.d) and requires .service/etc notation + (rc, out, err) = module.run_command("%s list-unit-files '%s'" % (systemctl, unit)) + if rc == 0: + is_systemd = True + else: + # Check for systemctl command + module.run_command(systemctl, check_rc=True) + + # Does service exist? + found = is_systemd or is_initd + if is_initd and not is_systemd: + module.warn('The service (%s) is actually an init script but the system is managed by systemd' % unit) + + # mask/unmask the service, if requested, can operate on services before they are installed + if module.params['masked'] is not None: + # state is not masked unless systemd affirms otherwise + (rc, out, err) = module.run_command("%s is-enabled '%s'" % (systemctl, unit)) + masked = out.strip() == "masked" + + if masked != module.params['masked']: + result['changed'] = True + if module.params['masked']: + action = 'mask' + else: + action = 'unmask' + + if not module.check_mode: + (rc, out, err) = module.run_command("%s %s '%s'" % (systemctl, action, unit)) + if rc != 0: + # some versions of system CAN mask/unmask non existing services, we only fail on missing if they don't + fail_if_missing(module, found, unit, msg='host') + + # Enable/disable service startup at boot if requested + if module.params['enabled'] is not None: + + if module.params['enabled']: + action = 'enable' + else: + action = 'disable' + + fail_if_missing(module, found, unit, msg='host') + + # do we need to enable the service? + enabled = False + (rc, out, err) = module.run_command("%s is-enabled '%s' -l" % (systemctl, unit)) + + # check systemctl result or if it is a init script + if rc == 0: + enabled = True + # Check if the service is indirect or alias and if out contains exactly 1 line of string 'indirect'/ 'alias' it's disabled + if out.splitlines() == ["indirect"] or out.splitlines() == ["alias"]: + enabled = False + + elif rc == 1: + # if not a user or global user service and both init script and unit file exist stdout should have enabled/disabled, otherwise use rc entries + if module.params['scope'] == 'system' and \ + is_initd and \ + not out.strip().endswith('disabled') and \ + sysv_is_enabled(unit): + enabled = True + + # default to current state + result['enabled'] = enabled + + # Change enable/disable if needed + if enabled != module.params['enabled']: + result['changed'] = True + if not module.check_mode: + (rc, out, err) = module.run_command("%s %s '%s'" % (systemctl, action, unit)) + if rc != 0: + module.fail_json(msg="Unable to %s service %s: %s" % (action, unit, out + err)) + + result['enabled'] = not enabled + + # set service state if requested + if module.params['state'] is not None: + fail_if_missing(module, found, unit, msg="host") + + # default to desired state + result['state'] = module.params['state'] + + # What is current service state? + if 'ActiveState' in result['status']: + action = None + if module.params['state'] == 'started': + if not is_running_service(result['status']): + action = 'start' + elif module.params['state'] == 'stopped': + if is_running_service(result['status']) or is_deactivating_service(result['status']): + action = 'stop' + else: + if not is_running_service(result['status']): + action = 'start' + else: + action = module.params['state'][:-2] # remove 'ed' from restarted/reloaded + result['state'] = 'started' + + if action: + result['changed'] = True + if not module.check_mode: + (rc, out, err) = module.run_command("%s %s '%s'" % (systemctl, action, unit)) + if rc != 0: + module.fail_json(msg="Unable to %s service %s: %s" % (action, unit, err)) + # check for chroot + elif is_chroot(module) or os.environ.get('SYSTEMD_OFFLINE') == '1': + module.warn("Target is a chroot or systemd is offline. This can lead to false positives or prevent the init system tools from working.") + else: + # this should not happen? + module.fail_json(msg="Service is in unknown state", status=result['status']) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt index 16763b35990..73fc8d41c0a 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -90,8 +90,9 @@ lib/ansible/modules/stat.py validate-modules:doc-default-does-not-match-spec # lib/ansible/modules/stat.py validate-modules:parameter-invalid lib/ansible/modules/stat.py validate-modules:parameter-type-not-in-doc lib/ansible/modules/stat.py validate-modules:undocumented-parameter -lib/ansible/modules/systemd.py validate-modules:parameter-invalid -lib/ansible/modules/systemd.py validate-modules:return-syntax-error +lib/ansible/modules/systemd_service.py validate-modules:module-incorrect-version-added +lib/ansible/modules/systemd_service.py validate-modules:parameter-invalid +lib/ansible/modules/systemd_service.py validate-modules:return-syntax-error lib/ansible/modules/sysvinit.py validate-modules:return-syntax-error lib/ansible/modules/uri.py validate-modules:doc-required-mismatch lib/ansible/modules/user.py validate-modules:doc-default-does-not-match-spec