From 8a754e0858d922e955e71b253c139e071ecec432 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 18:04:21 +0200 Subject: Adding upstream version 2.14.3. Signed-off-by: Daniel Baumann --- lib/ansible/module_utils/facts/system/__init__.py | 0 lib/ansible/module_utils/facts/system/apparmor.py | 41 ++ lib/ansible/module_utils/facts/system/caps.py | 62 ++ lib/ansible/module_utils/facts/system/chroot.py | 49 ++ lib/ansible/module_utils/facts/system/cmdline.py | 81 +++ lib/ansible/module_utils/facts/system/date_time.py | 70 ++ .../module_utils/facts/system/distribution.py | 726 +++++++++++++++++++++ lib/ansible/module_utils/facts/system/dns.py | 68 ++ lib/ansible/module_utils/facts/system/env.py | 39 ++ lib/ansible/module_utils/facts/system/fips.py | 39 ++ lib/ansible/module_utils/facts/system/loadavg.py | 31 + lib/ansible/module_utils/facts/system/local.py | 113 ++++ lib/ansible/module_utils/facts/system/lsb.py | 108 +++ lib/ansible/module_utils/facts/system/pkg_mgr.py | 165 +++++ lib/ansible/module_utils/facts/system/platform.py | 99 +++ lib/ansible/module_utils/facts/system/python.py | 62 ++ lib/ansible/module_utils/facts/system/selinux.py | 93 +++ .../module_utils/facts/system/service_mgr.py | 152 +++++ .../module_utils/facts/system/ssh_pub_keys.py | 56 ++ lib/ansible/module_utils/facts/system/user.py | 55 ++ 20 files changed, 2109 insertions(+) create mode 100644 lib/ansible/module_utils/facts/system/__init__.py create mode 100644 lib/ansible/module_utils/facts/system/apparmor.py create mode 100644 lib/ansible/module_utils/facts/system/caps.py create mode 100644 lib/ansible/module_utils/facts/system/chroot.py create mode 100644 lib/ansible/module_utils/facts/system/cmdline.py create mode 100644 lib/ansible/module_utils/facts/system/date_time.py create mode 100644 lib/ansible/module_utils/facts/system/distribution.py create mode 100644 lib/ansible/module_utils/facts/system/dns.py create mode 100644 lib/ansible/module_utils/facts/system/env.py create mode 100644 lib/ansible/module_utils/facts/system/fips.py create mode 100644 lib/ansible/module_utils/facts/system/loadavg.py create mode 100644 lib/ansible/module_utils/facts/system/local.py create mode 100644 lib/ansible/module_utils/facts/system/lsb.py create mode 100644 lib/ansible/module_utils/facts/system/pkg_mgr.py create mode 100644 lib/ansible/module_utils/facts/system/platform.py create mode 100644 lib/ansible/module_utils/facts/system/python.py create mode 100644 lib/ansible/module_utils/facts/system/selinux.py create mode 100644 lib/ansible/module_utils/facts/system/service_mgr.py create mode 100644 lib/ansible/module_utils/facts/system/ssh_pub_keys.py create mode 100644 lib/ansible/module_utils/facts/system/user.py (limited to 'lib/ansible/module_utils/facts/system') diff --git a/lib/ansible/module_utils/facts/system/__init__.py b/lib/ansible/module_utils/facts/system/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/ansible/module_utils/facts/system/apparmor.py b/lib/ansible/module_utils/facts/system/apparmor.py new file mode 100644 index 0000000..3b702f9 --- /dev/null +++ b/lib/ansible/module_utils/facts/system/apparmor.py @@ -0,0 +1,41 @@ +# Collect facts related to apparmor +# +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.facts.collector import BaseFactCollector + + +class ApparmorFactCollector(BaseFactCollector): + name = 'apparmor' + _fact_ids = set() # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + facts_dict = {} + apparmor_facts = {} + if os.path.exists('/sys/kernel/security/apparmor'): + apparmor_facts['status'] = 'enabled' + else: + apparmor_facts['status'] = 'disabled' + + facts_dict['apparmor'] = apparmor_facts + return facts_dict diff --git a/lib/ansible/module_utils/facts/system/caps.py b/lib/ansible/module_utils/facts/system/caps.py new file mode 100644 index 0000000..6a1e26d --- /dev/null +++ b/lib/ansible/module_utils/facts/system/caps.py @@ -0,0 +1,62 @@ +# Collect facts related to systems 'capabilities' via capsh +# +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils._text import to_text +from ansible.module_utils.facts.collector import BaseFactCollector + + +class SystemCapabilitiesFactCollector(BaseFactCollector): + name = 'caps' + _fact_ids = set(['system_capabilities', + 'system_capabilities_enforced']) # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + + rc = -1 + facts_dict = {'system_capabilities_enforced': 'N/A', + 'system_capabilities': 'N/A'} + if module: + capsh_path = module.get_bin_path('capsh') + if capsh_path: + # NOTE: -> get_caps_data()/parse_caps_data() for easier mocking -akl + try: + rc, out, err = module.run_command([capsh_path, "--print"], errors='surrogate_then_replace', handle_exceptions=False) + except (IOError, OSError) as e: + module.warn('Could not query system capabilities: %s' % str(e)) + + if rc == 0: + enforced_caps = [] + enforced = 'NA' + for line in out.splitlines(): + if len(line) < 1: + continue + if line.startswith('Current:'): + if line.split(':')[1].strip() == '=ep': + enforced = 'False' + else: + enforced = 'True' + enforced_caps = [i.strip() for i in line.split('=')[1].split(',')] + + facts_dict['system_capabilities_enforced'] = enforced + facts_dict['system_capabilities'] = enforced_caps + + return facts_dict diff --git a/lib/ansible/module_utils/facts/system/chroot.py b/lib/ansible/module_utils/facts/system/chroot.py new file mode 100644 index 0000000..94138a0 --- /dev/null +++ b/lib/ansible/module_utils/facts/system/chroot.py @@ -0,0 +1,49 @@ +# Copyright (c) 2017 Ansible Project +# 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 + +import os + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.facts.collector import BaseFactCollector + + +def is_chroot(module=None): + + is_chroot = None + + if os.environ.get('debian_chroot', False): + is_chroot = True + else: + my_root = os.stat('/') + try: + # check if my file system is the root one + proc_root = os.stat('/proc/1/root/.') + is_chroot = my_root.st_ino != proc_root.st_ino or my_root.st_dev != proc_root.st_dev + except Exception: + # I'm not root or no proc, fallback to checking it is inode #2 + fs_root_ino = 2 + + if module is not None: + stat_path = module.get_bin_path('stat') + if stat_path: + cmd = [stat_path, '-f', '--format=%T', '/'] + rc, out, err = module.run_command(cmd) + if 'btrfs' in out: + fs_root_ino = 256 + elif 'xfs' in out: + fs_root_ino = 128 + + is_chroot = (my_root.st_ino != fs_root_ino) + + return is_chroot + + +class ChrootFactCollector(BaseFactCollector): + name = 'chroot' + _fact_ids = set(['is_chroot']) # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + return {'is_chroot': is_chroot(module)} diff --git a/lib/ansible/module_utils/facts/system/cmdline.py b/lib/ansible/module_utils/facts/system/cmdline.py new file mode 100644 index 0000000..782186d --- /dev/null +++ b/lib/ansible/module_utils/facts/system/cmdline.py @@ -0,0 +1,81 @@ +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import shlex + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.facts.utils import get_file_content + +from ansible.module_utils.facts.collector import BaseFactCollector + + +class CmdLineFactCollector(BaseFactCollector): + name = 'cmdline' + _fact_ids = set() # type: t.Set[str] + + def _get_proc_cmdline(self): + return get_file_content('/proc/cmdline') + + def _parse_proc_cmdline(self, data): + cmdline_dict = {} + try: + for piece in shlex.split(data, posix=False): + item = piece.split('=', 1) + if len(item) == 1: + cmdline_dict[item[0]] = True + else: + cmdline_dict[item[0]] = item[1] + except ValueError: + pass + + return cmdline_dict + + def _parse_proc_cmdline_facts(self, data): + cmdline_dict = {} + try: + for piece in shlex.split(data, posix=False): + item = piece.split('=', 1) + if len(item) == 1: + cmdline_dict[item[0]] = True + else: + if item[0] in cmdline_dict: + if isinstance(cmdline_dict[item[0]], list): + cmdline_dict[item[0]].append(item[1]) + else: + new_list = [cmdline_dict[item[0]], item[1]] + cmdline_dict[item[0]] = new_list + else: + cmdline_dict[item[0]] = item[1] + except ValueError: + pass + + return cmdline_dict + + def collect(self, module=None, collected_facts=None): + cmdline_facts = {} + + data = self._get_proc_cmdline() + + if not data: + return cmdline_facts + + cmdline_facts['cmdline'] = self._parse_proc_cmdline(data) + cmdline_facts['proc_cmdline'] = self._parse_proc_cmdline_facts(data) + + return cmdline_facts diff --git a/lib/ansible/module_utils/facts/system/date_time.py b/lib/ansible/module_utils/facts/system/date_time.py new file mode 100644 index 0000000..481bef4 --- /dev/null +++ b/lib/ansible/module_utils/facts/system/date_time.py @@ -0,0 +1,70 @@ +# Data and time related facts collection for ansible. +# +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import datetime +import time + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.facts.collector import BaseFactCollector + + +class DateTimeFactCollector(BaseFactCollector): + name = 'date_time' + _fact_ids = set() # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + facts_dict = {} + date_time_facts = {} + + # Store the timestamp once, then get local and UTC versions from that + epoch_ts = time.time() + now = datetime.datetime.fromtimestamp(epoch_ts) + utcnow = datetime.datetime.utcfromtimestamp(epoch_ts) + + date_time_facts['year'] = now.strftime('%Y') + date_time_facts['month'] = now.strftime('%m') + date_time_facts['weekday'] = now.strftime('%A') + date_time_facts['weekday_number'] = now.strftime('%w') + date_time_facts['weeknumber'] = now.strftime('%W') + date_time_facts['day'] = now.strftime('%d') + date_time_facts['hour'] = now.strftime('%H') + date_time_facts['minute'] = now.strftime('%M') + date_time_facts['second'] = now.strftime('%S') + date_time_facts['epoch'] = now.strftime('%s') + # epoch returns float or string in some non-linux environments + if date_time_facts['epoch'] == '' or date_time_facts['epoch'][0] == '%': + date_time_facts['epoch'] = str(int(epoch_ts)) + # epoch_int always returns integer format of epoch + date_time_facts['epoch_int'] = str(int(now.strftime('%s'))) + if date_time_facts['epoch_int'] == '' or date_time_facts['epoch_int'][0] == '%': + date_time_facts['epoch_int'] = str(int(epoch_ts)) + date_time_facts['date'] = now.strftime('%Y-%m-%d') + date_time_facts['time'] = now.strftime('%H:%M:%S') + date_time_facts['iso8601_micro'] = utcnow.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + date_time_facts['iso8601'] = utcnow.strftime("%Y-%m-%dT%H:%M:%SZ") + date_time_facts['iso8601_basic'] = now.strftime("%Y%m%dT%H%M%S%f") + date_time_facts['iso8601_basic_short'] = now.strftime("%Y%m%dT%H%M%S") + date_time_facts['tz'] = time.strftime("%Z") + date_time_facts['tz_dst'] = time.tzname[1] + date_time_facts['tz_offset'] = time.strftime("%z") + + facts_dict['date_time'] = date_time_facts + return facts_dict diff --git a/lib/ansible/module_utils/facts/system/distribution.py b/lib/ansible/module_utils/facts/system/distribution.py new file mode 100644 index 0000000..dcb6e5a --- /dev/null +++ b/lib/ansible/module_utils/facts/system/distribution.py @@ -0,0 +1,726 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) Ansible Project +# 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 + +import os +import platform +import re + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.common.sys_info import get_distribution, get_distribution_version, \ + get_distribution_codename +from ansible.module_utils.facts.utils import get_file_content, get_file_lines +from ansible.module_utils.facts.collector import BaseFactCollector + + +def get_uname(module, flags=('-v')): + if isinstance(flags, str): + flags = flags.split() + command = ['uname'] + command.extend(flags) + rc, out, err = module.run_command(command) + if rc == 0: + return out + return None + + +def _file_exists(path, allow_empty=False): + # not finding the file, exit early + if not os.path.exists(path): + return False + + # if just the path needs to exists (ie, it can be empty) we are done + if allow_empty: + return True + + # file exists but is empty and we dont allow_empty + if os.path.getsize(path) == 0: + return False + + # file exists with some content + return True + + +class DistributionFiles: + '''has-a various distro file parsers (os-release, etc) and logic for finding the right one.''' + # every distribution name mentioned here, must have one of + # - allowempty == True + # - be listed in SEARCH_STRING + # - have a function get_distribution_DISTNAME implemented + # keep names in sync with Conditionals page of docs + OSDIST_LIST = ( + {'path': '/etc/altlinux-release', 'name': 'Altlinux'}, + {'path': '/etc/oracle-release', 'name': 'OracleLinux'}, + {'path': '/etc/slackware-version', 'name': 'Slackware'}, + {'path': '/etc/centos-release', 'name': 'CentOS'}, + {'path': '/etc/redhat-release', 'name': 'RedHat'}, + {'path': '/etc/vmware-release', 'name': 'VMwareESX', 'allowempty': True}, + {'path': '/etc/openwrt_release', 'name': 'OpenWrt'}, + {'path': '/etc/os-release', 'name': 'Amazon'}, + {'path': '/etc/system-release', 'name': 'Amazon'}, + {'path': '/etc/alpine-release', 'name': 'Alpine'}, + {'path': '/etc/arch-release', 'name': 'Archlinux', 'allowempty': True}, + {'path': '/etc/os-release', 'name': 'Archlinux'}, + {'path': '/etc/os-release', 'name': 'SUSE'}, + {'path': '/etc/SuSE-release', 'name': 'SUSE'}, + {'path': '/etc/gentoo-release', 'name': 'Gentoo'}, + {'path': '/etc/os-release', 'name': 'Debian'}, + {'path': '/etc/lsb-release', 'name': 'Debian'}, + {'path': '/etc/lsb-release', 'name': 'Mandriva'}, + {'path': '/etc/sourcemage-release', 'name': 'SMGL'}, + {'path': '/usr/lib/os-release', 'name': 'ClearLinux'}, + {'path': '/etc/coreos/update.conf', 'name': 'Coreos'}, + {'path': '/etc/os-release', 'name': 'Flatcar'}, + {'path': '/etc/os-release', 'name': 'NA'}, + ) + + SEARCH_STRING = { + 'OracleLinux': 'Oracle Linux', + 'RedHat': 'Red Hat', + 'Altlinux': 'ALT', + 'SMGL': 'Source Mage GNU/Linux', + } + + # We can't include this in SEARCH_STRING because a name match on its keys + # causes a fallback to using the first whitespace separated item from the file content + # as the name. For os-release, that is in form 'NAME=Arch' + OS_RELEASE_ALIAS = { + 'Archlinux': 'Arch Linux' + } + + STRIP_QUOTES = r'\'\"\\' + + def __init__(self, module): + self.module = module + + def _get_file_content(self, path): + return get_file_content(path) + + def _get_dist_file_content(self, path, allow_empty=False): + # cant find that dist file or it is incorrectly empty + if not _file_exists(path, allow_empty=allow_empty): + return False, None + + data = self._get_file_content(path) + return True, data + + def _parse_dist_file(self, name, dist_file_content, path, collected_facts): + dist_file_dict = {} + dist_file_content = dist_file_content.strip(DistributionFiles.STRIP_QUOTES) + if name in self.SEARCH_STRING: + # look for the distribution string in the data and replace according to RELEASE_NAME_MAP + # only the distribution name is set, the version is assumed to be correct from distro.linux_distribution() + if self.SEARCH_STRING[name] in dist_file_content: + # this sets distribution=RedHat if 'Red Hat' shows up in data + dist_file_dict['distribution'] = name + dist_file_dict['distribution_file_search_string'] = self.SEARCH_STRING[name] + else: + # this sets distribution to what's in the data, e.g. CentOS, Scientific, ... + dist_file_dict['distribution'] = dist_file_content.split()[0] + + return True, dist_file_dict + + if name in self.OS_RELEASE_ALIAS: + if self.OS_RELEASE_ALIAS[name] in dist_file_content: + dist_file_dict['distribution'] = name + return True, dist_file_dict + return False, dist_file_dict + + # call a dedicated function for parsing the file content + # TODO: replace with a map or a class + try: + # FIXME: most of these dont actually look at the dist file contents, but random other stuff + distfunc_name = 'parse_distribution_file_' + name + distfunc = getattr(self, distfunc_name) + parsed, dist_file_dict = distfunc(name, dist_file_content, path, collected_facts) + return parsed, dist_file_dict + except AttributeError as exc: + self.module.debug('exc: %s' % exc) + # this should never happen, but if it does fail quietly and not with a traceback + return False, dist_file_dict + + return True, dist_file_dict + # to debug multiple matching release files, one can use: + # self.facts['distribution_debug'].append({path + ' ' + name: + # (parsed, + # self.facts['distribution'], + # self.facts['distribution_version'], + # self.facts['distribution_release'], + # )}) + + def _guess_distribution(self): + # try to find out which linux distribution this is + dist = (get_distribution(), get_distribution_version(), get_distribution_codename()) + distribution_guess = { + 'distribution': dist[0] or 'NA', + 'distribution_version': dist[1] or 'NA', + # distribution_release can be the empty string + 'distribution_release': 'NA' if dist[2] is None else dist[2] + } + + distribution_guess['distribution_major_version'] = distribution_guess['distribution_version'].split('.')[0] or 'NA' + return distribution_guess + + def process_dist_files(self): + # Try to handle the exceptions now ... + # self.facts['distribution_debug'] = [] + dist_file_facts = {} + + dist_guess = self._guess_distribution() + dist_file_facts.update(dist_guess) + + for ddict in self.OSDIST_LIST: + name = ddict['name'] + path = ddict['path'] + allow_empty = ddict.get('allowempty', False) + + has_dist_file, dist_file_content = self._get_dist_file_content(path, allow_empty=allow_empty) + + # but we allow_empty. For example, ArchLinux with an empty /etc/arch-release and a + # /etc/os-release with a different name + if has_dist_file and allow_empty: + dist_file_facts['distribution'] = name + dist_file_facts['distribution_file_path'] = path + dist_file_facts['distribution_file_variety'] = name + break + + if not has_dist_file: + # keep looking + continue + + parsed_dist_file, parsed_dist_file_facts = self._parse_dist_file(name, dist_file_content, path, dist_file_facts) + + # finally found the right os dist file and were able to parse it + if parsed_dist_file: + dist_file_facts['distribution'] = name + dist_file_facts['distribution_file_path'] = path + # distribution and file_variety are the same here, but distribution + # will be changed/mapped to a more specific name. + # ie, dist=Fedora, file_variety=RedHat + dist_file_facts['distribution_file_variety'] = name + dist_file_facts['distribution_file_parsed'] = parsed_dist_file + dist_file_facts.update(parsed_dist_file_facts) + break + + return dist_file_facts + + # TODO: FIXME: split distro file parsing into its own module or class + def parse_distribution_file_Slackware(self, name, data, path, collected_facts): + slackware_facts = {} + if 'Slackware' not in data: + return False, slackware_facts # TODO: remove + slackware_facts['distribution'] = name + version = re.findall(r'\w+[.]\w+\+?', data) + if version: + slackware_facts['distribution_version'] = version[0] + return True, slackware_facts + + def parse_distribution_file_Amazon(self, name, data, path, collected_facts): + amazon_facts = {} + if 'Amazon' not in data: + return False, amazon_facts + amazon_facts['distribution'] = 'Amazon' + if path == '/etc/os-release': + version = re.search(r"VERSION_ID=\"(.*)\"", data) + if version: + distribution_version = version.group(1) + amazon_facts['distribution_version'] = distribution_version + version_data = distribution_version.split(".") + if len(version_data) > 1: + major, minor = version_data + else: + major, minor = version_data[0], 'NA' + + amazon_facts['distribution_major_version'] = major + amazon_facts['distribution_minor_version'] = minor + else: + version = [n for n in data.split() if n.isdigit()] + version = version[0] if version else 'NA' + amazon_facts['distribution_version'] = version + + return True, amazon_facts + + def parse_distribution_file_OpenWrt(self, name, data, path, collected_facts): + openwrt_facts = {} + if 'OpenWrt' not in data: + return False, openwrt_facts # TODO: remove + openwrt_facts['distribution'] = name + version = re.search('DISTRIB_RELEASE="(.*)"', data) + if version: + openwrt_facts['distribution_version'] = version.groups()[0] + release = re.search('DISTRIB_CODENAME="(.*)"', data) + if release: + openwrt_facts['distribution_release'] = release.groups()[0] + return True, openwrt_facts + + def parse_distribution_file_Alpine(self, name, data, path, collected_facts): + alpine_facts = {} + alpine_facts['distribution'] = 'Alpine' + alpine_facts['distribution_version'] = data + return True, alpine_facts + + def parse_distribution_file_SUSE(self, name, data, path, collected_facts): + suse_facts = {} + if 'suse' not in data.lower(): + return False, suse_facts # TODO: remove if tested without this + if path == '/etc/os-release': + for line in data.splitlines(): + distribution = re.search("^NAME=(.*)", line) + if distribution: + suse_facts['distribution'] = distribution.group(1).strip('"') + # example pattern are 13.04 13.0 13 + distribution_version = re.search(r'^VERSION_ID="?([0-9]+\.?[0-9]*)"?', line) + if distribution_version: + suse_facts['distribution_version'] = distribution_version.group(1) + suse_facts['distribution_major_version'] = distribution_version.group(1).split('.')[0] + if 'open' in data.lower(): + release = re.search(r'^VERSION_ID="?[0-9]+\.?([0-9]*)"?', line) + if release: + suse_facts['distribution_release'] = release.groups()[0] + elif 'enterprise' in data.lower() and 'VERSION_ID' in line: + # SLES doesn't got funny release names + release = re.search(r'^VERSION_ID="?[0-9]+\.?([0-9]*)"?', line) + if release.group(1): + release = release.group(1) + else: + release = "0" # no minor number, so it is the first release + suse_facts['distribution_release'] = release + elif path == '/etc/SuSE-release': + if 'open' in data.lower(): + data = data.splitlines() + distdata = get_file_content(path).splitlines()[0] + suse_facts['distribution'] = distdata.split()[0] + for line in data: + release = re.search('CODENAME *= *([^\n]+)', line) + if release: + suse_facts['distribution_release'] = release.groups()[0].strip() + elif 'enterprise' in data.lower(): + lines = data.splitlines() + distribution = lines[0].split()[0] + if "Server" in data: + suse_facts['distribution'] = "SLES" + elif "Desktop" in data: + suse_facts['distribution'] = "SLED" + for line in lines: + release = re.search('PATCHLEVEL = ([0-9]+)', line) # SLES doesn't got funny release names + if release: + suse_facts['distribution_release'] = release.group(1) + suse_facts['distribution_version'] = collected_facts['distribution_version'] + '.' + release.group(1) + + # See https://www.suse.com/support/kb/doc/?id=000019341 for SLES for SAP + if os.path.islink('/etc/products.d/baseproduct') and os.path.realpath('/etc/products.d/baseproduct').endswith('SLES_SAP.prod'): + suse_facts['distribution'] = 'SLES_SAP' + + return True, suse_facts + + def parse_distribution_file_Debian(self, name, data, path, collected_facts): + debian_facts = {} + if 'Debian' in data or 'Raspbian' in data: + debian_facts['distribution'] = 'Debian' + release = re.search(r"PRETTY_NAME=[^(]+ \(?([^)]+?)\)", data) + if release: + debian_facts['distribution_release'] = release.groups()[0] + + # Last resort: try to find release from tzdata as either lsb is missing or this is very old debian + if collected_facts['distribution_release'] == 'NA' and 'Debian' in data: + dpkg_cmd = self.module.get_bin_path('dpkg') + if dpkg_cmd: + cmd = "%s --status tzdata|grep Provides|cut -f2 -d'-'" % dpkg_cmd + rc, out, err = self.module.run_command(cmd) + if rc == 0: + debian_facts['distribution_release'] = out.strip() + debian_version_path = '/etc/debian_version' + distdata = get_file_lines(debian_version_path) + for line in distdata: + m = re.search(r'(\d+)\.(\d+)', line.strip()) + if m: + debian_facts['distribution_minor_version'] = m.groups()[1] + elif 'Ubuntu' in data: + debian_facts['distribution'] = 'Ubuntu' + # nothing else to do, Ubuntu gets correct info from python functions + elif 'SteamOS' in data: + debian_facts['distribution'] = 'SteamOS' + # nothing else to do, SteamOS gets correct info from python functions + elif path in ('/etc/lsb-release', '/etc/os-release') and ('Kali' in data or 'Parrot' in data): + if 'Kali' in data: + # Kali does not provide /etc/lsb-release anymore + debian_facts['distribution'] = 'Kali' + elif 'Parrot' in data: + debian_facts['distribution'] = 'Parrot' + release = re.search('DISTRIB_RELEASE=(.*)', data) + if release: + debian_facts['distribution_release'] = release.groups()[0] + elif 'Devuan' in data: + debian_facts['distribution'] = 'Devuan' + release = re.search(r"PRETTY_NAME=\"?[^(\"]+ \(?([^) \"]+)\)?", data) + if release: + debian_facts['distribution_release'] = release.groups()[0] + version = re.search(r"VERSION_ID=\"(.*)\"", data) + if version: + debian_facts['distribution_version'] = version.group(1) + debian_facts['distribution_major_version'] = version.group(1) + elif 'Cumulus' in data: + debian_facts['distribution'] = 'Cumulus Linux' + version = re.search(r"VERSION_ID=(.*)", data) + if version: + major, _minor, _dummy_ver = version.group(1).split(".") + debian_facts['distribution_version'] = version.group(1) + debian_facts['distribution_major_version'] = major + + release = re.search(r'VERSION="(.*)"', data) + if release: + debian_facts['distribution_release'] = release.groups()[0] + elif "Mint" in data: + debian_facts['distribution'] = 'Linux Mint' + version = re.search(r"VERSION_ID=\"(.*)\"", data) + if version: + debian_facts['distribution_version'] = version.group(1) + debian_facts['distribution_major_version'] = version.group(1).split('.')[0] + elif 'UOS' in data or 'Uos' in data or 'uos' in data: + debian_facts['distribution'] = 'Uos' + release = re.search(r"VERSION_CODENAME=\"?([^\"]+)\"?", data) + if release: + debian_facts['distribution_release'] = release.groups()[0] + version = re.search(r"VERSION_ID=\"(.*)\"", data) + if version: + debian_facts['distribution_version'] = version.group(1) + debian_facts['distribution_major_version'] = version.group(1).split('.')[0] + elif 'Deepin' in data or 'deepin' in data: + debian_facts['distribution'] = 'Deepin' + release = re.search(r"VERSION_CODENAME=\"?([^\"]+)\"?", data) + if release: + debian_facts['distribution_release'] = release.groups()[0] + version = re.search(r"VERSION_ID=\"(.*)\"", data) + if version: + debian_facts['distribution_version'] = version.group(1) + debian_facts['distribution_major_version'] = version.group(1).split('.')[0] + else: + return False, debian_facts + + return True, debian_facts + + def parse_distribution_file_Mandriva(self, name, data, path, collected_facts): + mandriva_facts = {} + if 'Mandriva' in data: + mandriva_facts['distribution'] = 'Mandriva' + version = re.search('DISTRIB_RELEASE="(.*)"', data) + if version: + mandriva_facts['distribution_version'] = version.groups()[0] + release = re.search('DISTRIB_CODENAME="(.*)"', data) + if release: + mandriva_facts['distribution_release'] = release.groups()[0] + mandriva_facts['distribution'] = name + else: + return False, mandriva_facts + + return True, mandriva_facts + + def parse_distribution_file_NA(self, name, data, path, collected_facts): + na_facts = {} + for line in data.splitlines(): + distribution = re.search("^NAME=(.*)", line) + if distribution and name == 'NA': + na_facts['distribution'] = distribution.group(1).strip('"') + version = re.search("^VERSION=(.*)", line) + if version and collected_facts['distribution_version'] == 'NA': + na_facts['distribution_version'] = version.group(1).strip('"') + return True, na_facts + + def parse_distribution_file_Coreos(self, name, data, path, collected_facts): + coreos_facts = {} + # FIXME: pass in ro copy of facts for this kind of thing + distro = get_distribution() + + if distro.lower() == 'coreos': + if not data: + # include fix from #15230, #15228 + # TODO: verify this is ok for above bugs + return False, coreos_facts + release = re.search("^GROUP=(.*)", data) + if release: + coreos_facts['distribution_release'] = release.group(1).strip('"') + else: + return False, coreos_facts # TODO: remove if tested without this + + return True, coreos_facts + + def parse_distribution_file_Flatcar(self, name, data, path, collected_facts): + flatcar_facts = {} + distro = get_distribution() + + if distro.lower() != 'flatcar': + return False, flatcar_facts + + if not data: + return False, flatcar_facts + + version = re.search("VERSION=(.*)", data) + if version: + flatcar_facts['distribution_major_version'] = version.group(1).strip('"').split('.')[0] + flatcar_facts['distribution_version'] = version.group(1).strip('"') + + return True, flatcar_facts + + def parse_distribution_file_ClearLinux(self, name, data, path, collected_facts): + clear_facts = {} + if "clearlinux" not in name.lower(): + return False, clear_facts + + pname = re.search('NAME="(.*)"', data) + if pname: + if 'Clear Linux' not in pname.groups()[0]: + return False, clear_facts + clear_facts['distribution'] = pname.groups()[0] + version = re.search('VERSION_ID=(.*)', data) + if version: + clear_facts['distribution_major_version'] = version.groups()[0] + clear_facts['distribution_version'] = version.groups()[0] + release = re.search('ID=(.*)', data) + if release: + clear_facts['distribution_release'] = release.groups()[0] + return True, clear_facts + + def parse_distribution_file_CentOS(self, name, data, path, collected_facts): + centos_facts = {} + + if 'CentOS Stream' in data: + centos_facts['distribution_release'] = 'Stream' + return True, centos_facts + + if "TencentOS Server" in data: + centos_facts['distribution'] = 'TencentOS' + return True, centos_facts + + return False, centos_facts + + +class Distribution(object): + """ + This subclass of Facts fills the distribution, distribution_version and distribution_release variables + + To do so it checks the existence and content of typical files in /etc containing distribution information + + This is unit tested. Please extend the tests to cover all distributions if you have them available. + """ + + # keep keys in sync with Conditionals page of docs + OS_FAMILY_MAP = {'RedHat': ['RedHat', 'RHEL', 'Fedora', 'CentOS', 'Scientific', 'SLC', + 'Ascendos', 'CloudLinux', 'PSBM', 'OracleLinux', 'OVS', + 'OEL', 'Amazon', 'Virtuozzo', 'XenServer', 'Alibaba', + 'EulerOS', 'openEuler', 'AlmaLinux', 'Rocky', 'TencentOS', + 'EuroLinux', 'Kylin Linux Advanced Server'], + 'Debian': ['Debian', 'Ubuntu', 'Raspbian', 'Neon', 'KDE neon', + 'Linux Mint', 'SteamOS', 'Devuan', 'Kali', 'Cumulus Linux', + 'Pop!_OS', 'Parrot', 'Pardus GNU/Linux', 'Uos', 'Deepin', 'OSMC'], + 'Suse': ['SuSE', 'SLES', 'SLED', 'openSUSE', 'openSUSE Tumbleweed', + 'SLES_SAP', 'SUSE_LINUX', 'openSUSE Leap'], + 'Archlinux': ['Archlinux', 'Antergos', 'Manjaro'], + 'Mandrake': ['Mandrake', 'Mandriva'], + 'Solaris': ['Solaris', 'Nexenta', 'OmniOS', 'OpenIndiana', 'SmartOS'], + 'Slackware': ['Slackware'], + 'Altlinux': ['Altlinux'], + 'SGML': ['SGML'], + 'Gentoo': ['Gentoo', 'Funtoo'], + 'Alpine': ['Alpine'], + 'AIX': ['AIX'], + 'HP-UX': ['HPUX'], + 'Darwin': ['MacOSX'], + 'FreeBSD': ['FreeBSD', 'TrueOS'], + 'ClearLinux': ['Clear Linux OS', 'Clear Linux Mix'], + 'DragonFly': ['DragonflyBSD', 'DragonFlyBSD', 'Gentoo/DragonflyBSD', 'Gentoo/DragonFlyBSD'], + 'NetBSD': ['NetBSD'], } + + OS_FAMILY = {} + for family, names in OS_FAMILY_MAP.items(): + for name in names: + OS_FAMILY[name] = family + + def __init__(self, module): + self.module = module + + def get_distribution_facts(self): + distribution_facts = {} + + # The platform module provides information about the running + # system/distribution. Use this as a baseline and fix buggy systems + # afterwards + system = platform.system() + distribution_facts['distribution'] = system + distribution_facts['distribution_release'] = platform.release() + distribution_facts['distribution_version'] = platform.version() + + systems_implemented = ('AIX', 'HP-UX', 'Darwin', 'FreeBSD', 'OpenBSD', 'SunOS', 'DragonFly', 'NetBSD') + + if system in systems_implemented: + cleanedname = system.replace('-', '') + distfunc = getattr(self, 'get_distribution_' + cleanedname) + dist_func_facts = distfunc() + distribution_facts.update(dist_func_facts) + elif system == 'Linux': + + distribution_files = DistributionFiles(module=self.module) + + # linux_distribution_facts = LinuxDistribution(module).get_distribution_facts() + dist_file_facts = distribution_files.process_dist_files() + + distribution_facts.update(dist_file_facts) + + distro = distribution_facts['distribution'] + + # look for a os family alias for the 'distribution', if there isnt one, use 'distribution' + distribution_facts['os_family'] = self.OS_FAMILY.get(distro, None) or distro + + return distribution_facts + + def get_distribution_AIX(self): + aix_facts = {} + rc, out, err = self.module.run_command("/usr/bin/oslevel") + data = out.split('.') + aix_facts['distribution_major_version'] = data[0] + if len(data) > 1: + aix_facts['distribution_version'] = '%s.%s' % (data[0], data[1]) + aix_facts['distribution_release'] = data[1] + else: + aix_facts['distribution_version'] = data[0] + return aix_facts + + def get_distribution_HPUX(self): + hpux_facts = {} + rc, out, err = self.module.run_command(r"/usr/sbin/swlist |egrep 'HPUX.*OE.*[AB].[0-9]+\.[0-9]+'", use_unsafe_shell=True) + data = re.search(r'HPUX.*OE.*([AB].[0-9]+\.[0-9]+)\.([0-9]+).*', out) + if data: + hpux_facts['distribution_version'] = data.groups()[0] + hpux_facts['distribution_release'] = data.groups()[1] + return hpux_facts + + def get_distribution_Darwin(self): + darwin_facts = {} + darwin_facts['distribution'] = 'MacOSX' + rc, out, err = self.module.run_command("/usr/bin/sw_vers -productVersion") + data = out.split()[-1] + if data: + darwin_facts['distribution_major_version'] = data.split('.')[0] + darwin_facts['distribution_version'] = data + return darwin_facts + + def get_distribution_FreeBSD(self): + freebsd_facts = {} + freebsd_facts['distribution_release'] = platform.release() + data = re.search(r'(\d+)\.(\d+)-(RELEASE|STABLE|CURRENT|RC|PRERELEASE).*', freebsd_facts['distribution_release']) + if 'trueos' in platform.version(): + freebsd_facts['distribution'] = 'TrueOS' + if data: + freebsd_facts['distribution_major_version'] = data.group(1) + freebsd_facts['distribution_version'] = '%s.%s' % (data.group(1), data.group(2)) + return freebsd_facts + + def get_distribution_OpenBSD(self): + openbsd_facts = {} + openbsd_facts['distribution_version'] = platform.release() + rc, out, err = self.module.run_command("/sbin/sysctl -n kern.version") + match = re.match(r'OpenBSD\s[0-9]+.[0-9]+-(\S+)\s.*', out) + if match: + openbsd_facts['distribution_release'] = match.groups()[0] + else: + openbsd_facts['distribution_release'] = 'release' + return openbsd_facts + + def get_distribution_DragonFly(self): + dragonfly_facts = { + 'distribution_release': platform.release() + } + rc, out, dummy = self.module.run_command("/sbin/sysctl -n kern.version") + match = re.search(r'v(\d+)\.(\d+)\.(\d+)-(RELEASE|STABLE|CURRENT).*', out) + if match: + dragonfly_facts['distribution_major_version'] = match.group(1) + dragonfly_facts['distribution_version'] = '%s.%s.%s' % match.groups()[:3] + return dragonfly_facts + + def get_distribution_NetBSD(self): + netbsd_facts = {} + platform_release = platform.release() + netbsd_facts['distribution_release'] = platform_release + rc, out, dummy = self.module.run_command("/sbin/sysctl -n kern.version") + match = re.match(r'NetBSD\s(\d+)\.(\d+)\s\((GENERIC)\).*', out) + if match: + netbsd_facts['distribution_major_version'] = match.group(1) + netbsd_facts['distribution_version'] = '%s.%s' % match.groups()[:2] + else: + netbsd_facts['distribution_major_version'] = platform_release.split('.')[0] + netbsd_facts['distribution_version'] = platform_release + return netbsd_facts + + def get_distribution_SMGL(self): + smgl_facts = {} + smgl_facts['distribution'] = 'Source Mage GNU/Linux' + return smgl_facts + + def get_distribution_SunOS(self): + sunos_facts = {} + + data = get_file_content('/etc/release').splitlines()[0] + + if 'Solaris' in data: + # for solaris 10 uname_r will contain 5.10, for solaris 11 it will have 5.11 + uname_r = get_uname(self.module, flags=['-r']) + ora_prefix = '' + if 'Oracle Solaris' in data: + data = data.replace('Oracle ', '') + ora_prefix = 'Oracle ' + sunos_facts['distribution'] = data.split()[0] + sunos_facts['distribution_version'] = data.split()[1] + sunos_facts['distribution_release'] = ora_prefix + data + sunos_facts['distribution_major_version'] = uname_r.split('.')[1].rstrip() + return sunos_facts + + uname_v = get_uname(self.module, flags=['-v']) + distribution_version = None + + if 'SmartOS' in data: + sunos_facts['distribution'] = 'SmartOS' + if _file_exists('/etc/product'): + product_data = dict([l.split(': ', 1) for l in get_file_content('/etc/product').splitlines() if ': ' in l]) + if 'Image' in product_data: + distribution_version = product_data.get('Image').split()[-1] + elif 'OpenIndiana' in data: + sunos_facts['distribution'] = 'OpenIndiana' + elif 'OmniOS' in data: + sunos_facts['distribution'] = 'OmniOS' + distribution_version = data.split()[-1] + elif uname_v is not None and 'NexentaOS_' in uname_v: + sunos_facts['distribution'] = 'Nexenta' + distribution_version = data.split()[-1].lstrip('v') + + if sunos_facts.get('distribution', '') in ('SmartOS', 'OpenIndiana', 'OmniOS', 'Nexenta'): + sunos_facts['distribution_release'] = data.strip() + if distribution_version is not None: + sunos_facts['distribution_version'] = distribution_version + elif uname_v is not None: + sunos_facts['distribution_version'] = uname_v.splitlines()[0].strip() + return sunos_facts + + return sunos_facts + + +class DistributionFactCollector(BaseFactCollector): + name = 'distribution' + _fact_ids = set(['distribution_version', + 'distribution_release', + 'distribution_major_version', + 'os_family']) # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + collected_facts = collected_facts or {} + facts_dict = {} + if not module: + return facts_dict + + distribution = Distribution(module=module) + distro_facts = distribution.get_distribution_facts() + + return distro_facts diff --git a/lib/ansible/module_utils/facts/system/dns.py b/lib/ansible/module_utils/facts/system/dns.py new file mode 100644 index 0000000..d913f4a --- /dev/null +++ b/lib/ansible/module_utils/facts/system/dns.py @@ -0,0 +1,68 @@ +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.facts.utils import get_file_content + +from ansible.module_utils.facts.collector import BaseFactCollector + + +class DnsFactCollector(BaseFactCollector): + name = 'dns' + _fact_ids = set() # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + dns_facts = {} + + # TODO: flatten + dns_facts['dns'] = {} + + for line in get_file_content('/etc/resolv.conf', '').splitlines(): + if line.startswith('#') or line.startswith(';') or line.strip() == '': + continue + tokens = line.split() + if len(tokens) == 0: + continue + if tokens[0] == 'nameserver': + if 'nameservers' not in dns_facts['dns']: + dns_facts['dns']['nameservers'] = [] + for nameserver in tokens[1:]: + dns_facts['dns']['nameservers'].append(nameserver) + elif tokens[0] == 'domain': + if len(tokens) > 1: + dns_facts['dns']['domain'] = tokens[1] + elif tokens[0] == 'search': + dns_facts['dns']['search'] = [] + for suffix in tokens[1:]: + dns_facts['dns']['search'].append(suffix) + elif tokens[0] == 'sortlist': + dns_facts['dns']['sortlist'] = [] + for address in tokens[1:]: + dns_facts['dns']['sortlist'].append(address) + elif tokens[0] == 'options': + dns_facts['dns']['options'] = {} + if len(tokens) > 1: + for option in tokens[1:]: + option_tokens = option.split(':', 1) + if len(option_tokens) == 0: + continue + val = len(option_tokens) == 2 and option_tokens[1] or True + dns_facts['dns']['options'][option_tokens[0]] = val + + return dns_facts diff --git a/lib/ansible/module_utils/facts/system/env.py b/lib/ansible/module_utils/facts/system/env.py new file mode 100644 index 0000000..605443f --- /dev/null +++ b/lib/ansible/module_utils/facts/system/env.py @@ -0,0 +1,39 @@ +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.six import iteritems + +from ansible.module_utils.facts.collector import BaseFactCollector + + +class EnvFactCollector(BaseFactCollector): + name = 'env' + _fact_ids = set() # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + env_facts = {} + env_facts['env'] = {} + + for k, v in iteritems(os.environ): + env_facts['env'][k] = v + + return env_facts diff --git a/lib/ansible/module_utils/facts/system/fips.py b/lib/ansible/module_utils/facts/system/fips.py new file mode 100644 index 0000000..7e56610 --- /dev/null +++ b/lib/ansible/module_utils/facts/system/fips.py @@ -0,0 +1,39 @@ +# Determine if a system is in 'fips' mode +# +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.facts.utils import get_file_content + +from ansible.module_utils.facts.collector import BaseFactCollector + + +class FipsFactCollector(BaseFactCollector): + name = 'fips' + _fact_ids = set() # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + # NOTE: this is populated even if it is not set + fips_facts = {} + fips_facts['fips'] = False + data = get_file_content('/proc/sys/crypto/fips_enabled') + if data and data == '1': + fips_facts['fips'] = True + return fips_facts diff --git a/lib/ansible/module_utils/facts/system/loadavg.py b/lib/ansible/module_utils/facts/system/loadavg.py new file mode 100644 index 0000000..8475f2a --- /dev/null +++ b/lib/ansible/module_utils/facts/system/loadavg.py @@ -0,0 +1,31 @@ +# (c) 2021 Ansible Project +# 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 + +import os + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.facts.collector import BaseFactCollector + + +class LoadAvgFactCollector(BaseFactCollector): + name = 'loadavg' + _fact_ids = set() # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + facts = {} + try: + # (0.58, 0.82, 0.98) + loadavg = os.getloadavg() + facts['loadavg'] = { + '1m': loadavg[0], + '5m': loadavg[1], + '15m': loadavg[2] + } + except OSError: + pass + + return facts diff --git a/lib/ansible/module_utils/facts/system/local.py b/lib/ansible/module_utils/facts/system/local.py new file mode 100644 index 0000000..bacdbe0 --- /dev/null +++ b/lib/ansible/module_utils/facts/system/local.py @@ -0,0 +1,113 @@ +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import glob +import json +import os +import stat + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils._text import to_text +from ansible.module_utils.facts.utils import get_file_content +from ansible.module_utils.facts.collector import BaseFactCollector +from ansible.module_utils.six.moves import configparser, StringIO + + +class LocalFactCollector(BaseFactCollector): + name = 'local' + _fact_ids = set() # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + local_facts = {} + local_facts['local'] = {} + + if not module: + return local_facts + + fact_path = module.params.get('fact_path', None) + + if not fact_path or not os.path.exists(fact_path): + return local_facts + + local = {} + # go over .fact files, run executables, read rest, skip bad with warning and note + for fn in sorted(glob.glob(fact_path + '/*.fact')): + # use filename for key where it will sit under local facts + fact_base = os.path.basename(fn).replace('.fact', '') + failed = None + try: + executable_fact = stat.S_IXUSR & os.stat(fn)[stat.ST_MODE] + except OSError as e: + failed = 'Could not stat fact (%s): %s' % (fn, to_text(e)) + local[fact_base] = failed + module.warn(failed) + continue + if executable_fact: + try: + # run it + rc, out, err = module.run_command(fn) + if rc != 0: + failed = 'Failure executing fact script (%s), rc: %s, err: %s' % (fn, rc, err) + except (IOError, OSError) as e: + failed = 'Could not execute fact script (%s): %s' % (fn, to_text(e)) + + if failed is not None: + local[fact_base] = failed + module.warn(failed) + continue + else: + # ignores exceptions and returns empty + out = get_file_content(fn, default='') + + try: + # ensure we have unicode + out = to_text(out, errors='surrogate_or_strict') + except UnicodeError: + fact = 'error loading fact - output of running "%s" was not utf-8' % fn + local[fact_base] = fact + module.warn(fact) + continue + + # try to read it as json first + try: + fact = json.loads(out) + except ValueError: + # if that fails read it with ConfigParser + cp = configparser.ConfigParser() + try: + cp.readfp(StringIO(out)) + except configparser.Error: + fact = "error loading facts as JSON or ini - please check content: %s" % fn + module.warn(fact) + else: + fact = {} + for sect in cp.sections(): + if sect not in fact: + fact[sect] = {} + for opt in cp.options(sect): + val = cp.get(sect, opt) + fact[sect][opt] = val + except Exception as e: + fact = "Failed to convert (%s) to JSON: %s" % (fn, to_text(e)) + module.warn(fact) + + local[fact_base] = fact + + local_facts['local'] = local + return local_facts diff --git a/lib/ansible/module_utils/facts/system/lsb.py b/lib/ansible/module_utils/facts/system/lsb.py new file mode 100644 index 0000000..2dc1433 --- /dev/null +++ b/lib/ansible/module_utils/facts/system/lsb.py @@ -0,0 +1,108 @@ +# Collect facts related to LSB (Linux Standard Base) +# +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.facts.utils import get_file_lines +from ansible.module_utils.facts.collector import BaseFactCollector + + +class LSBFactCollector(BaseFactCollector): + name = 'lsb' + _fact_ids = set() # type: t.Set[str] + STRIP_QUOTES = r'\'\"\\' + + def _lsb_release_bin(self, lsb_path, module): + lsb_facts = {} + + if not lsb_path: + return lsb_facts + + rc, out, err = module.run_command([lsb_path, "-a"], errors='surrogate_then_replace') + if rc != 0: + return lsb_facts + + for line in out.splitlines(): + if len(line) < 1 or ':' not in line: + continue + value = line.split(':', 1)[1].strip() + + if 'LSB Version:' in line: + lsb_facts['release'] = value + elif 'Distributor ID:' in line: + lsb_facts['id'] = value + elif 'Description:' in line: + lsb_facts['description'] = value + elif 'Release:' in line: + lsb_facts['release'] = value + elif 'Codename:' in line: + lsb_facts['codename'] = value + + return lsb_facts + + def _lsb_release_file(self, etc_lsb_release_location): + lsb_facts = {} + + if not os.path.exists(etc_lsb_release_location): + return lsb_facts + + for line in get_file_lines(etc_lsb_release_location): + value = line.split('=', 1)[1].strip() + + if 'DISTRIB_ID' in line: + lsb_facts['id'] = value + elif 'DISTRIB_RELEASE' in line: + lsb_facts['release'] = value + elif 'DISTRIB_DESCRIPTION' in line: + lsb_facts['description'] = value + elif 'DISTRIB_CODENAME' in line: + lsb_facts['codename'] = value + + return lsb_facts + + def collect(self, module=None, collected_facts=None): + facts_dict = {} + lsb_facts = {} + + if not module: + return facts_dict + + lsb_path = module.get_bin_path('lsb_release') + + # try the 'lsb_release' script first + if lsb_path: + lsb_facts = self._lsb_release_bin(lsb_path, + module=module) + + # no lsb_release, try looking in /etc/lsb-release + if not lsb_facts: + lsb_facts = self._lsb_release_file('/etc/lsb-release') + + if lsb_facts and 'release' in lsb_facts: + lsb_facts['major_release'] = lsb_facts['release'].split('.')[0] + + for k, v in lsb_facts.items(): + if v: + lsb_facts[k] = v.strip(LSBFactCollector.STRIP_QUOTES) + + facts_dict['lsb'] = lsb_facts + return facts_dict diff --git a/lib/ansible/module_utils/facts/system/pkg_mgr.py b/lib/ansible/module_utils/facts/system/pkg_mgr.py new file mode 100644 index 0000000..704ea20 --- /dev/null +++ b/lib/ansible/module_utils/facts/system/pkg_mgr.py @@ -0,0 +1,165 @@ +# Collect facts related to the system package manager + +# 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 + +import os +import subprocess + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.facts.collector import BaseFactCollector + +# A list of dicts. If there is a platform with more than one +# package manager, put the preferred one last. If there is an +# ansible module, use that as the value for the 'name' key. +PKG_MGRS = [{'path': '/usr/bin/rpm-ostree', 'name': 'atomic_container'}, + {'path': '/usr/bin/yum', 'name': 'yum'}, + {'path': '/usr/bin/dnf', 'name': 'dnf'}, + {'path': '/usr/bin/apt-get', 'name': 'apt'}, + {'path': '/usr/bin/zypper', 'name': 'zypper'}, + {'path': '/usr/sbin/urpmi', 'name': 'urpmi'}, + {'path': '/usr/bin/pacman', 'name': 'pacman'}, + {'path': '/bin/opkg', 'name': 'opkg'}, + {'path': '/usr/pkg/bin/pkgin', 'name': 'pkgin'}, + {'path': '/opt/local/bin/pkgin', 'name': 'pkgin'}, + {'path': '/opt/tools/bin/pkgin', 'name': 'pkgin'}, + {'path': '/opt/local/bin/port', 'name': 'macports'}, + {'path': '/usr/local/bin/brew', 'name': 'homebrew'}, + {'path': '/opt/homebrew/bin/brew', 'name': 'homebrew'}, + {'path': '/sbin/apk', 'name': 'apk'}, + {'path': '/usr/sbin/pkg', 'name': 'pkgng'}, + {'path': '/usr/sbin/swlist', 'name': 'swdepot'}, + {'path': '/usr/bin/emerge', 'name': 'portage'}, + {'path': '/usr/sbin/pkgadd', 'name': 'svr4pkg'}, + {'path': '/usr/bin/pkg', 'name': 'pkg5'}, + {'path': '/usr/bin/xbps-install', 'name': 'xbps'}, + {'path': '/usr/local/sbin/pkg', 'name': 'pkgng'}, + {'path': '/usr/bin/swupd', 'name': 'swupd'}, + {'path': '/usr/sbin/sorcery', 'name': 'sorcery'}, + {'path': '/usr/bin/installp', 'name': 'installp'}, + {'path': '/QOpenSys/pkgs/bin/yum', 'name': 'yum'}, + ] + + +class OpenBSDPkgMgrFactCollector(BaseFactCollector): + name = 'pkg_mgr' + _fact_ids = set() # type: t.Set[str] + _platform = 'OpenBSD' + + def collect(self, module=None, collected_facts=None): + facts_dict = {} + + facts_dict['pkg_mgr'] = 'openbsd_pkg' + return facts_dict + + +# the fact ends up being 'pkg_mgr' so stick with that naming/spelling +class PkgMgrFactCollector(BaseFactCollector): + name = 'pkg_mgr' + _fact_ids = set() # type: t.Set[str] + _platform = 'Generic' + required_facts = set(['distribution']) + + def _pkg_mgr_exists(self, pkg_mgr_name): + for cur_pkg_mgr in [pkg_mgr for pkg_mgr in PKG_MGRS if pkg_mgr['name'] == pkg_mgr_name]: + if os.path.exists(cur_pkg_mgr['path']): + return pkg_mgr_name + + def _check_rh_versions(self, pkg_mgr_name, collected_facts): + if os.path.exists('/run/ostree-booted'): + return "atomic_container" + + if collected_facts['ansible_distribution'] == 'Fedora': + try: + if int(collected_facts['ansible_distribution_major_version']) < 23: + if self._pkg_mgr_exists('yum'): + pkg_mgr_name = 'yum' + + else: + if self._pkg_mgr_exists('dnf'): + pkg_mgr_name = 'dnf' + except ValueError: + # If there's some new magical Fedora version in the future, + # just default to dnf + pkg_mgr_name = 'dnf' + elif collected_facts['ansible_distribution'] == 'Amazon': + try: + if int(collected_facts['ansible_distribution_major_version']) < 2022: + if self._pkg_mgr_exists('yum'): + pkg_mgr_name = 'yum' + else: + if self._pkg_mgr_exists('dnf'): + pkg_mgr_name = 'dnf' + except ValueError: + pkg_mgr_name = 'dnf' + else: + # If it's not one of the above and it's Red Hat family of distros, assume + # RHEL or a clone. For versions of RHEL < 8 that Ansible supports, the + # vendor supported official package manager is 'yum' and in RHEL 8+ + # (as far as we know at the time of this writing) it is 'dnf'. + # If anyone wants to force a non-official package manager then they + # can define a provider to either the package or yum action plugins. + if int(collected_facts['ansible_distribution_major_version']) < 8: + pkg_mgr_name = 'yum' + else: + pkg_mgr_name = 'dnf' + return pkg_mgr_name + + def _check_apt_flavor(self, pkg_mgr_name): + # Check if '/usr/bin/apt' is APT-RPM or an ordinary (dpkg-based) APT. + # There's rpm package on Debian, so checking if /usr/bin/rpm exists + # is not enough. Instead ask RPM if /usr/bin/apt-get belongs to some + # RPM package. + rpm_query = '/usr/bin/rpm -q --whatprovides /usr/bin/apt-get'.split() + if os.path.exists('/usr/bin/rpm'): + with open(os.devnull, 'w') as null: + try: + subprocess.check_call(rpm_query, stdout=null, stderr=null) + pkg_mgr_name = 'apt_rpm' + except subprocess.CalledProcessError: + # No apt-get in RPM database. Looks like Debian/Ubuntu + # with rpm package installed + pkg_mgr_name = 'apt' + return pkg_mgr_name + + def pkg_mgrs(self, collected_facts): + # Filter out the /usr/bin/pkg because on Altlinux it is actually the + # perl-Package (not Solaris package manager). + # Since the pkg5 takes precedence over apt, this workaround + # is required to select the suitable package manager on Altlinux. + if collected_facts['ansible_os_family'] == 'Altlinux': + return filter(lambda pkg: pkg['path'] != '/usr/bin/pkg', PKG_MGRS) + else: + return PKG_MGRS + + def collect(self, module=None, collected_facts=None): + facts_dict = {} + collected_facts = collected_facts or {} + + pkg_mgr_name = 'unknown' + for pkg in self.pkg_mgrs(collected_facts): + if os.path.exists(pkg['path']): + pkg_mgr_name = pkg['name'] + + # Handle distro family defaults when more than one package manager is + # installed or available to the distro, the ansible_fact entry should be + # the default package manager officially supported by the distro. + if collected_facts['ansible_os_family'] == "RedHat": + pkg_mgr_name = self._check_rh_versions(pkg_mgr_name, collected_facts) + elif collected_facts['ansible_os_family'] == 'Debian' and pkg_mgr_name != 'apt': + # It's possible to install yum, dnf, zypper, rpm, etc inside of + # Debian. Doing so does not mean the system wants to use them. + pkg_mgr_name = 'apt' + elif collected_facts['ansible_os_family'] == 'Altlinux': + if pkg_mgr_name == 'apt': + pkg_mgr_name = 'apt_rpm' + + # Check if /usr/bin/apt-get is ordinary (dpkg-based) APT or APT-RPM + if pkg_mgr_name == 'apt': + pkg_mgr_name = self._check_apt_flavor(pkg_mgr_name) + + facts_dict['pkg_mgr'] = pkg_mgr_name + return facts_dict diff --git a/lib/ansible/module_utils/facts/system/platform.py b/lib/ansible/module_utils/facts/system/platform.py new file mode 100644 index 0000000..b947801 --- /dev/null +++ b/lib/ansible/module_utils/facts/system/platform.py @@ -0,0 +1,99 @@ +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re +import socket +import platform + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.facts.utils import get_file_content + +from ansible.module_utils.facts.collector import BaseFactCollector + +# i86pc is a Solaris and derivatives-ism +SOLARIS_I86_RE_PATTERN = r'i([3456]86|86pc)' +solaris_i86_re = re.compile(SOLARIS_I86_RE_PATTERN) + + +class PlatformFactCollector(BaseFactCollector): + name = 'platform' + _fact_ids = set(['system', + 'kernel', + 'kernel_version', + 'machine', + 'python_version', + 'architecture', + 'machine_id']) # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + platform_facts = {} + # platform.system() can be Linux, Darwin, Java, or Windows + platform_facts['system'] = platform.system() + platform_facts['kernel'] = platform.release() + platform_facts['kernel_version'] = platform.version() + platform_facts['machine'] = platform.machine() + + platform_facts['python_version'] = platform.python_version() + + platform_facts['fqdn'] = socket.getfqdn() + platform_facts['hostname'] = platform.node().split('.')[0] + platform_facts['nodename'] = platform.node() + + platform_facts['domain'] = '.'.join(platform_facts['fqdn'].split('.')[1:]) + + arch_bits = platform.architecture()[0] + + platform_facts['userspace_bits'] = arch_bits.replace('bit', '') + if platform_facts['machine'] == 'x86_64': + platform_facts['architecture'] = platform_facts['machine'] + if platform_facts['userspace_bits'] == '64': + platform_facts['userspace_architecture'] = 'x86_64' + elif platform_facts['userspace_bits'] == '32': + platform_facts['userspace_architecture'] = 'i386' + elif solaris_i86_re.search(platform_facts['machine']): + platform_facts['architecture'] = 'i386' + if platform_facts['userspace_bits'] == '64': + platform_facts['userspace_architecture'] = 'x86_64' + elif platform_facts['userspace_bits'] == '32': + platform_facts['userspace_architecture'] = 'i386' + else: + platform_facts['architecture'] = platform_facts['machine'] + + if platform_facts['system'] == 'AIX': + # Attempt to use getconf to figure out architecture + # fall back to bootinfo if needed + getconf_bin = module.get_bin_path('getconf') + if getconf_bin: + rc, out, err = module.run_command([getconf_bin, 'MACHINE_ARCHITECTURE']) + data = out.splitlines() + platform_facts['architecture'] = data[0] + else: + bootinfo_bin = module.get_bin_path('bootinfo') + rc, out, err = module.run_command([bootinfo_bin, '-p']) + data = out.splitlines() + platform_facts['architecture'] = data[0] + elif platform_facts['system'] == 'OpenBSD': + platform_facts['architecture'] = platform.uname()[5] + + machine_id = get_file_content("/var/lib/dbus/machine-id") or get_file_content("/etc/machine-id") + if machine_id: + machine_id = machine_id.splitlines()[0] + platform_facts["machine_id"] = machine_id + + return platform_facts diff --git a/lib/ansible/module_utils/facts/system/python.py b/lib/ansible/module_utils/facts/system/python.py new file mode 100644 index 0000000..50b66dd --- /dev/null +++ b/lib/ansible/module_utils/facts/system/python.py @@ -0,0 +1,62 @@ +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import sys + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.facts.collector import BaseFactCollector + +try: + # Check if we have SSLContext support + from ssl import create_default_context, SSLContext + del create_default_context + del SSLContext + HAS_SSLCONTEXT = True +except ImportError: + HAS_SSLCONTEXT = False + + +class PythonFactCollector(BaseFactCollector): + name = 'python' + _fact_ids = set() # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + python_facts = {} + python_facts['python'] = { + 'version': { + 'major': sys.version_info[0], + 'minor': sys.version_info[1], + 'micro': sys.version_info[2], + 'releaselevel': sys.version_info[3], + 'serial': sys.version_info[4] + }, + 'version_info': list(sys.version_info), + 'executable': sys.executable, + 'has_sslcontext': HAS_SSLCONTEXT + } + + try: + python_facts['python']['type'] = sys.subversion[0] + except AttributeError: + try: + python_facts['python']['type'] = sys.implementation.name + except AttributeError: + python_facts['python']['type'] = None + + return python_facts diff --git a/lib/ansible/module_utils/facts/system/selinux.py b/lib/ansible/module_utils/facts/system/selinux.py new file mode 100644 index 0000000..5c6b012 --- /dev/null +++ b/lib/ansible/module_utils/facts/system/selinux.py @@ -0,0 +1,93 @@ +# Collect facts related to selinux +# +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.facts.collector import BaseFactCollector + +try: + from ansible.module_utils.compat import selinux + HAVE_SELINUX = True +except ImportError: + HAVE_SELINUX = False + +SELINUX_MODE_DICT = { + 1: 'enforcing', + 0: 'permissive', + -1: 'disabled' +} + + +class SelinuxFactCollector(BaseFactCollector): + name = 'selinux' + _fact_ids = set() # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + facts_dict = {} + selinux_facts = {} + + # If selinux library is missing, only set the status and selinux_python_present since + # there is no way to tell if SELinux is enabled or disabled on the system + # without the library. + if not HAVE_SELINUX: + selinux_facts['status'] = 'Missing selinux Python library' + facts_dict['selinux'] = selinux_facts + facts_dict['selinux_python_present'] = False + return facts_dict + + # Set a boolean for testing whether the Python library is present + facts_dict['selinux_python_present'] = True + + if not selinux.is_selinux_enabled(): + selinux_facts['status'] = 'disabled' + else: + selinux_facts['status'] = 'enabled' + + try: + selinux_facts['policyvers'] = selinux.security_policyvers() + except (AttributeError, OSError): + selinux_facts['policyvers'] = 'unknown' + + try: + (rc, configmode) = selinux.selinux_getenforcemode() + if rc == 0: + selinux_facts['config_mode'] = SELINUX_MODE_DICT.get(configmode, 'unknown') + else: + selinux_facts['config_mode'] = 'unknown' + except (AttributeError, OSError): + selinux_facts['config_mode'] = 'unknown' + + try: + mode = selinux.security_getenforce() + selinux_facts['mode'] = SELINUX_MODE_DICT.get(mode, 'unknown') + except (AttributeError, OSError): + selinux_facts['mode'] = 'unknown' + + try: + (rc, policytype) = selinux.selinux_getpolicytype() + if rc == 0: + selinux_facts['type'] = policytype + else: + selinux_facts['type'] = 'unknown' + except (AttributeError, OSError): + selinux_facts['type'] = 'unknown' + + facts_dict['selinux'] = selinux_facts + return facts_dict diff --git a/lib/ansible/module_utils/facts/system/service_mgr.py b/lib/ansible/module_utils/facts/system/service_mgr.py new file mode 100644 index 0000000..d862ac9 --- /dev/null +++ b/lib/ansible/module_utils/facts/system/service_mgr.py @@ -0,0 +1,152 @@ +# Collect facts related to system service manager and init. +# +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import platform +import re + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils._text import to_native + +from ansible.module_utils.facts.utils import get_file_content +from ansible.module_utils.facts.collector import BaseFactCollector + +# The distutils module is not shipped with SUNWPython on Solaris. +# It's in the SUNWPython-devel package which also contains development files +# that don't belong on production boxes. Since our Solaris code doesn't +# depend on LooseVersion, do not import it on Solaris. +if platform.system() != 'SunOS': + from ansible.module_utils.compat.version import LooseVersion + + +class ServiceMgrFactCollector(BaseFactCollector): + name = 'service_mgr' + _fact_ids = set() # type: t.Set[str] + required_facts = set(['platform', 'distribution']) + + @staticmethod + def is_systemd_managed(module): + # tools must be installed + if module.get_bin_path('systemctl'): + + # this should show if systemd is the boot init system, if checking init faild to mark as systemd + # these mirror systemd's own sd_boot test http://www.freedesktop.org/software/systemd/man/sd_booted.html + for canary in ["/run/systemd/system/", "/dev/.run/systemd/", "/dev/.systemd/"]: + if os.path.exists(canary): + return True + return False + + @staticmethod + def is_systemd_managed_offline(module): + # tools must be installed + if module.get_bin_path('systemctl'): + # check if /sbin/init is a symlink to systemd + # on SUSE, /sbin/init may be missing if systemd-sysvinit package is not installed. + if os.path.islink('/sbin/init') and os.path.basename(os.readlink('/sbin/init')) == 'systemd': + return True + return False + + def collect(self, module=None, collected_facts=None): + facts_dict = {} + + if not module: + return facts_dict + + collected_facts = collected_facts or {} + service_mgr_name = None + + # TODO: detect more custom init setups like bootscripts, dmd, s6, Epoch, etc + # also other OSs other than linux might need to check across several possible candidates + + # Mapping of proc_1 values to more useful names + proc_1_map = { + 'procd': 'openwrt_init', + 'runit-init': 'runit', + 'svscan': 'svc', + 'openrc-init': 'openrc', + } + + # try various forms of querying pid 1 + proc_1 = get_file_content('/proc/1/comm') + if proc_1 is None: + rc, proc_1, err = module.run_command("ps -p 1 -o comm|tail -n 1", use_unsafe_shell=True) + + # if command fails, or stdout is empty string or the output of the command starts with what looks like a PID, + # then the 'ps' command probably didn't work the way we wanted, probably because it's busybox + if rc != 0 or not proc_1.strip() or re.match(r' *[0-9]+ ', proc_1): + proc_1 = None + + # The ps command above may return "COMMAND" if the user cannot read /proc, e.g. with grsecurity + if proc_1 == "COMMAND\n": + proc_1 = None + + if proc_1 is None and os.path.islink('/sbin/init'): + proc_1 = os.readlink('/sbin/init') + + if proc_1 is not None: + proc_1 = os.path.basename(proc_1) + proc_1 = to_native(proc_1) + proc_1 = proc_1.strip() + + if proc_1 is not None and (proc_1 == 'init' or proc_1.endswith('sh')): + # many systems return init, so this cannot be trusted, if it ends in 'sh' it probalby is a shell in a container + proc_1 = None + + # if not init/None it should be an identifiable or custom init, so we are done! + if proc_1 is not None: + # Lookup proc_1 value in map and use proc_1 value itself if no match + service_mgr_name = proc_1_map.get(proc_1, proc_1) + + # start with the easy ones + elif collected_facts.get('ansible_distribution', None) == 'MacOSX': + # FIXME: find way to query executable, version matching is not ideal + if LooseVersion(platform.mac_ver()[0]) >= LooseVersion('10.4'): + service_mgr_name = 'launchd' + else: + service_mgr_name = 'systemstarter' + elif 'BSD' in collected_facts.get('ansible_system', '') or collected_facts.get('ansible_system') in ['Bitrig', 'DragonFly']: + # FIXME: we might want to break out to individual BSDs or 'rc' + service_mgr_name = 'bsdinit' + elif collected_facts.get('ansible_system') == 'AIX': + service_mgr_name = 'src' + elif collected_facts.get('ansible_system') == 'SunOS': + service_mgr_name = 'smf' + elif collected_facts.get('ansible_distribution') == 'OpenWrt': + service_mgr_name = 'openwrt_init' + elif collected_facts.get('ansible_system') == 'Linux': + # FIXME: mv is_systemd_managed + if self.is_systemd_managed(module=module): + service_mgr_name = 'systemd' + elif module.get_bin_path('initctl') and os.path.exists("/etc/init/"): + service_mgr_name = 'upstart' + elif os.path.exists('/sbin/openrc'): + service_mgr_name = 'openrc' + elif self.is_systemd_managed_offline(module=module): + service_mgr_name = 'systemd' + elif os.path.exists('/etc/init.d/'): + service_mgr_name = 'sysvinit' + + if not service_mgr_name: + # if we cannot detect, fallback to generic 'service' + service_mgr_name = 'service' + + facts_dict['service_mgr'] = service_mgr_name + return facts_dict diff --git a/lib/ansible/module_utils/facts/system/ssh_pub_keys.py b/lib/ansible/module_utils/facts/system/ssh_pub_keys.py new file mode 100644 index 0000000..85691c7 --- /dev/null +++ b/lib/ansible/module_utils/facts/system/ssh_pub_keys.py @@ -0,0 +1,56 @@ +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.facts.utils import get_file_content + +from ansible.module_utils.facts.collector import BaseFactCollector + + +class SshPubKeyFactCollector(BaseFactCollector): + name = 'ssh_pub_keys' + _fact_ids = set(['ssh_host_pub_keys', + 'ssh_host_key_dsa_public', + 'ssh_host_key_rsa_public', + 'ssh_host_key_ecdsa_public', + 'ssh_host_key_ed25519_public']) # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + ssh_pub_key_facts = {} + algos = ('dsa', 'rsa', 'ecdsa', 'ed25519') + + # list of directories to check for ssh keys + # used in the order listed here, the first one with keys is used + keydirs = ['/etc/ssh', '/etc/openssh', '/etc'] + + for keydir in keydirs: + for algo in algos: + factname = 'ssh_host_key_%s_public' % algo + if factname in ssh_pub_key_facts: + # a previous keydir was already successful, stop looking + # for keys + return ssh_pub_key_facts + key_filename = '%s/ssh_host_%s_key.pub' % (keydir, algo) + keydata = get_file_content(key_filename) + if keydata is not None: + (keytype, key) = keydata.split()[0:2] + ssh_pub_key_facts[factname] = key + ssh_pub_key_facts[factname + '_keytype'] = keytype + + return ssh_pub_key_facts diff --git a/lib/ansible/module_utils/facts/system/user.py b/lib/ansible/module_utils/facts/system/user.py new file mode 100644 index 0000000..2efa993 --- /dev/null +++ b/lib/ansible/module_utils/facts/system/user.py @@ -0,0 +1,55 @@ +# 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 . + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import getpass +import os +import pwd + +import ansible.module_utils.compat.typing as t + +from ansible.module_utils.facts.collector import BaseFactCollector + + +class UserFactCollector(BaseFactCollector): + name = 'user' + _fact_ids = set(['user_id', 'user_uid', 'user_gid', + 'user_gecos', 'user_dir', 'user_shell', + 'real_user_id', 'effective_user_id', + 'effective_group_ids']) # type: t.Set[str] + + def collect(self, module=None, collected_facts=None): + user_facts = {} + + user_facts['user_id'] = getpass.getuser() + + try: + pwent = pwd.getpwnam(getpass.getuser()) + except KeyError: + pwent = pwd.getpwuid(os.getuid()) + + user_facts['user_uid'] = pwent.pw_uid + user_facts['user_gid'] = pwent.pw_gid + user_facts['user_gecos'] = pwent.pw_gecos + user_facts['user_dir'] = pwent.pw_dir + user_facts['user_shell'] = pwent.pw_shell + user_facts['real_user_id'] = os.getuid() + user_facts['effective_user_id'] = os.geteuid() + user_facts['real_group_id'] = os.getgid() + user_facts['effective_group_id'] = os.getgid() + + return user_facts -- cgit v1.2.3