From dd44c0a0f2c9f1996efa387a3a903c902a8ad821 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 19 Mar 2015 13:21:45 -0400 Subject: [PATCH] Initial implementation of the service scanner module --- .../commands/run_fact_cache_receiver.py | 58 ++++++++- awx/plugins/library/scan_packages.py | 2 + awx/plugins/library/scan_services.py | 118 ++++++++++++++++++ 3 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 awx/plugins/library/scan_services.py diff --git a/awx/main/management/commands/run_fact_cache_receiver.py b/awx/main/management/commands/run_fact_cache_receiver.py index 29742cae3e..827ce5adaa 100644 --- a/awx/main/management/commands/run_fact_cache_receiver.py +++ b/awx/main/management/commands/run_fact_cache_receiver.py @@ -12,16 +12,69 @@ from pymongo import MongoClient logger = logging.getLogger('awx.main.commands.run_fact_cache_receiver') +from pymongo.son_manipulator import SONManipulator + +class KeyTransform(SONManipulator): + """Transforms keys going to database and restores them coming out. + + This allows keys with dots in them to be used (but does break searching on + them unless the find command also uses the transform. + + Example & test: + # To allow `.` (dots) in keys + import pymongo + client = pymongo.MongoClient("mongodb://localhost") + db = client['delete_me'] + db.add_son_manipulator(KeyTransform(".", "_dot_")) + db['mycol'].remove() + db['mycol'].update({'_id': 1}, {'127.0.0.1': 'localhost'}, upsert=True, + manipulate=True) + print db['mycol'].find().next() + print db['mycol'].find({'127_dot_0_dot_0_dot_1': 'localhost'}).next() + + Note: transformation could be easily extended to be more complex. + """ + + def __init__(self, replace, replacement): + self.replace = replace + self.replacement = replacement + + def transform_key(self, key): + """Transform key for saving to database.""" + return key.replace(self.replace, self.replacement) + + def revert_key(self, key): + """Restore transformed key returning from database.""" + return key.replace(self.replacement, self.replace) + + def transform_incoming(self, son, collection): + """Recursively replace all keys that need transforming.""" + for (key, value) in son.items(): + if self.replace in key: + if isinstance(value, dict): + son[self.transform_key(key)] = self.transform_incoming( + son.pop(key), collection) + else: + son[self.transform_key(key)] = son.pop(key) + elif isinstance(value, dict): # recurse into sub-docs + son[key] = self.transform_incoming(value, collection) + return son + + def transform_outgoing(self, son, collection): + return son + class FactCacheReceiver(object): def __init__(self): self.client = MongoClient('localhost', 27017) - + def process_fact_message(self, message): - host = message['host'] + host = message['host'].replace(".", "_") facts = message['facts'] date_key = message['date_key'] host_db = self.client.host_facts + host_db.add_son_manipulator(KeyTransform(".", "_")) + host_db.add_son_manipulator(KeyTransform("$", "_")) host_collection = host_db[host] facts.update(dict(tower_host=host, datetime=date_key)) rec = host_collection.find({"datetime": date_key}) @@ -35,7 +88,6 @@ class FactCacheReceiver(object): def run_receiver(self): with Socket('fact_cache', 'r') as facts: for message in facts.listen(): - print("Message received: " + str(message)) if 'host' not in message or 'facts' not in message or 'date_key' not in message: continue self.process_fact_message(message) diff --git a/awx/plugins/library/scan_packages.py b/awx/plugins/library/scan_packages.py index 6925e38296..8fb2a68008 100755 --- a/awx/plugins/library/scan_packages.py +++ b/awx/plugins/library/scan_packages.py @@ -39,6 +39,8 @@ def main(): argument_spec = dict()) packages = [] + # TODO: module_utils/basic.py in ansible contains get_distribution() and get_distribution_version() + # which can be used here and is accessible by this script instead of this basic detector. if os.path.exists("/etc/redhat-release"): packages = rpm_package_list() elif os.path.exists("/etc/os-release"): diff --git a/awx/plugins/library/scan_services.py b/awx/plugins/library/scan_services.py new file mode 100644 index 0000000000..aced5f1ed3 --- /dev/null +++ b/awx/plugins/library/scan_services.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python + +import re +from ansible.module_utils.basic import * # noqa + +class BaseService(object): + + def __init__(self, module): + self.module = module + +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") + # Upstart and sysvinit + if initctl_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:]) + service_state = "running" if line_data[1] == "+" else "stopped" + services[service_name] = {"name": service_name, "state": service_state, "source": "sysv"} + rc, stdout, stderr = self.module.run_command("%s list" % initctl_path) + real_stdout = stdout.replace("\r","") + for line in real_stdout.split("\n"): + line_data = line.split() + if len(line_data) < 2: + continue + service_name = line_data[0] + if line_data[1].find("/") == -1: # we expect this to look like: start/running + continue + service_goal = line_data[1].split("/")[0] + service_state = line_data[1].split("/")[1].replace(",","") + if len(line_data) > 3: # If there's a pid associated with the service it'll be on the end of this string "process 418" + if line_data[2] == 'process': + pid = line_data[3] + else: + pid = None + else: + pid = None + 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 + rc, stdout, stderr = self.module.run_command('%s --status-all | grep -E "dead|is (running|stopped)"' % service_path, use_unsafe_shell=True) + for line in stdout.split("\n"): + line_data = line.split() + if re.match(".+\(pid.+[0-9]+\).+is running", line) is not None and len(line_data) == 5: + service_name = line_data[0] + service_pid = line_data[2].replace(")","") + service_state = "running" + elif len(line_data) > 2 and line_data[1] == "dead": + service_name = line_data[0] + service_pid = None + service_state = "dead" + elif len(line_data) == 3: + service_name = line_data[0] + service_pid = None + service_state = "stopped" + else: + continue + service_data = {"name": service_name, "state": service_state, "source": "sysv"} + services[service_name] = service_data + # rc, stdout, stderr = self.module.run_command("%s --list" % chkconfig_path) + # Do something with chkconfig status + 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 + services[line_data[0]] = {"name": line_data[0], "state": "running" if line_data[1] == "enabled" else "stopped"} + return services + +def main(): + module = AnsibleModule(argument_spec = dict()) + service_modules = (ServiceScanService, SystemctlScanService) + all_services = {} + for svc_module in service_modules: + svcmod = svc_module(module) + svc = svcmod.gather_services() + if svc is not None: + all_services.update(svc) + results = dict(ansible_facts=dict(services=all_services)) + module.exit_json(**results) + +main()