From 39edee7572c3e7b8d5734cbc8630f458604a2c11 Mon Sep 17 00:00:00 2001 From: Adam Miller Date: Mon, 8 Jan 2018 13:35:16 -0800 Subject: [PATCH] service_facts module (#34065) Signed-off-by: Adam Miller --- lib/ansible/modules/system/service_facts.py | 216 ++++++++++++++++++ .../integration/targets/service_facts/aliases | 3 + .../targets/service_facts/tasks/main.yml | 24 ++ 3 files changed, 243 insertions(+) create mode 100755 lib/ansible/modules/system/service_facts.py create mode 100644 test/integration/targets/service_facts/aliases create mode 100644 test/integration/targets/service_facts/tasks/main.yml diff --git a/lib/ansible/modules/system/service_facts.py b/lib/ansible/modules/system/service_facts.py new file mode 100755 index 00000000000..ddf22d8670e --- /dev/null +++ b/lib/ansible/modules/system/service_facts.py @@ -0,0 +1,216 @@ +#!/usr/bin/python +# (c) 2017, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# originally copied from AWX's scan_services module to bring this functionality +# into Core + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: service_facts +short_description: Return service state information as fact data +description: + - Return service state information as fact data for various service management utilities +version_added: "2.5" +requirements: ["Any of the following supported init systems: systemd, sysv, upstart"] + +author: + - Matthew Jones + - Adam Miller (@maxamillion) +''' + +EXAMPLES = ''' +- name: populate service facts + service_facts: + +- debug: + var: ansible_facts.services + +''' + +RETURN = ''' +ansible_facts: + description: facts to add to ansible_facts about the services on the system + returned: always + type: complex + contains: + "services": { + "network": { + "source": "sysv", + "state": "running", + "name": "network" + }, + arp-ethers.service: { + "source": "systemd", + "state": "stopped", + "name": "arp-ethers.service" + } + } +''' + + +import re +from ansible.module_utils.basic import AnsibleModule + + +class BaseService(object): + + def __init__(self, module): + self.module = module + self.incomplete_warning = False + + +class ServiceScanService(BaseService): + + def gather_services(self): + services = {} + service_path = self.module.get_bin_path("service") + if service_path is None: + return None + initctl_path = self.module.get_bin_path("initctl") + chkconfig_path = self.module.get_bin_path("chkconfig") + + # sysvinit + if service_path is not None and chkconfig_path is None: + rc, stdout, stderr = self.module.run_command("%s --status-all 2>&1 | grep -E \"\\[ (\\+|\\-) \\]\"" % service_path, use_unsafe_shell=True) + for line in stdout.split("\n"): + line_data = line.split() + if len(line_data) < 4: + continue # Skipping because we expected more data + service_name = " ".join(line_data[3:]) + if line_data[1] == "+": + service_state = "running" + else: + service_state = "stopped" + services[service_name] = {"name": service_name, "state": service_state, "source": "sysv"} + + # Upstart + if initctl_path is not None and chkconfig_path is None: + p = re.compile(r'^\s?(?P.*)\s(?P\w+)\/(?P\w+)(\,\sprocess\s(?P[0-9]+))?\s*$') + rc, stdout, stderr = self.module.run_command("%s list" % initctl_path) + real_stdout = stdout.replace("\r", "") + for line in real_stdout.split("\n"): + m = p.match(line) + if not m: + continue + service_name = m.group('name') + service_goal = m.group('goal') + service_state = m.group('state') + if m.group('pid'): + pid = m.group('pid') + else: + pid = None # NOQA + payload = {"name": service_name, "state": service_state, "goal": service_goal, "source": "upstart"} + services[service_name] = payload + + # RH sysvinit + elif chkconfig_path is not None: + # print '%s --status-all | grep -E "is (running|stopped)"' % service_path + p = re.compile( + r'(?P.*?)\s+[0-9]:(?Pon|off)\s+[0-9]:(?Pon|off)\s+[0-9]:(?Pon|off)\s+' + r'[0-9]:(?Pon|off)\s+[0-9]:(?Pon|off)\s+[0-9]:(?Pon|off)\s+[0-9]:(?Pon|off)') + rc, stdout, stderr = self.module.run_command('%s' % chkconfig_path, use_unsafe_shell=True) + # Check for special cases where stdout does not fit pattern + match_any = False + for line in stdout.split('\n'): + if p.match(line): + match_any = True + if not match_any: + p_simple = re.compile(r'(?P.*?)\s+(?Pon|off)') + match_any = False + for line in stdout.split('\n'): + if p_simple.match(line): + match_any = True + if match_any: + # Try extra flags " -l --allservices" needed for SLES11 + rc, stdout, stderr = self.module.run_command('%s -l --allservices' % chkconfig_path, use_unsafe_shell=True) + elif '--list' in stderr: + # Extra flag needed for RHEL5 + rc, stdout, stderr = self.module.run_command('%s --list' % chkconfig_path, use_unsafe_shell=True) + for line in stdout.split('\n'): + m = p.match(line) + if m: + service_name = m.group('service') + service_state = 'stopped' + if m.group('rl3') == 'on': + rc, stdout, stderr = self.module.run_command('%s %s status' % (service_path, service_name), use_unsafe_shell=True) + service_state = rc + if rc in (0,): + service_state = 'running' + # elif rc in (1,3): + else: + if 'root' in stderr or 'permission' in stderr.lower() or 'not in sudoers' in stderr.lower(): + self.incomplete_warning = True + continue + else: + service_state = 'stopped' + service_data = {"name": service_name, "state": service_state, "source": "sysv"} + services[service_name] = service_data + return services + + +class SystemctlScanService(BaseService): + + def systemd_enabled(self): + # Check if init is the systemd command, using comm as cmdline could be symlink + try: + f = open('/proc/1/comm', 'r') + except IOError: + # If comm doesn't exist, old kernel, no systemd + return False + for line in f: + if 'systemd' in line: + return True + return False + + def gather_services(self): + services = {} + if not self.systemd_enabled(): + return None + systemctl_path = self.module.get_bin_path("systemctl", opt_dirs=["/usr/bin", "/usr/local/bin"]) + if systemctl_path is None: + return None + rc, stdout, stderr = self.module.run_command("%s list-unit-files --type=service | tail -n +2 | head -n -2" % systemctl_path, use_unsafe_shell=True) + for line in stdout.split("\n"): + line_data = line.split() + if len(line_data) != 2: + continue + if line_data[1] == "enabled": + state_val = "running" + else: + state_val = "stopped" + services[line_data[0]] = {"name": line_data[0], "state": state_val, "source": "systemd"} + return services + + +def main(): + module = AnsibleModule(argument_spec=dict()) + service_modules = (ServiceScanService, SystemctlScanService) + all_services = {} + incomplete_warning = False + for svc_module in service_modules: + svcmod = svc_module(module) + svc = svcmod.gather_services() + if svc is not None: + all_services.update(svc) + if svcmod.incomplete_warning: + incomplete_warning = True + if len(all_services) == 0: + results = dict(skipped=True, msg="Failed to find any services. Sometimes this is due to insufficient privileges.") + else: + results = dict(ansible_facts=dict(services=all_services)) + if incomplete_warning: + results['msg'] = "WARNING: Could not find status for all services. Sometimes this is due to insufficient privileges." + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/service_facts/aliases b/test/integration/targets/service_facts/aliases new file mode 100644 index 00000000000..a553e79913f --- /dev/null +++ b/test/integration/targets/service_facts/aliases @@ -0,0 +1,3 @@ +posix/ci/group3 +skip/freebsd +skip/osx diff --git a/test/integration/targets/service_facts/tasks/main.yml b/test/integration/targets/service_facts/tasks/main.yml new file mode 100644 index 00000000000..65220289d93 --- /dev/null +++ b/test/integration/targets/service_facts/tasks/main.yml @@ -0,0 +1,24 @@ +# Test playbook for the service_facts module +# (c) 2017, Adam Miller + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +- name: Gather service facts + service_facts: + +- name: check for ansible_facts.services exists + assert: + that: ansible_facts.services is defined