summaryrefslogtreecommitdiffstats
path: root/lib/ansible/module_utils/facts/system
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/module_utils/facts/system')
-rw-r--r--lib/ansible/module_utils/facts/system/__init__.py0
-rw-r--r--lib/ansible/module_utils/facts/system/apparmor.py41
-rw-r--r--lib/ansible/module_utils/facts/system/caps.py62
-rw-r--r--lib/ansible/module_utils/facts/system/chroot.py49
-rw-r--r--lib/ansible/module_utils/facts/system/cmdline.py81
-rw-r--r--lib/ansible/module_utils/facts/system/date_time.py70
-rw-r--r--lib/ansible/module_utils/facts/system/distribution.py726
-rw-r--r--lib/ansible/module_utils/facts/system/dns.py68
-rw-r--r--lib/ansible/module_utils/facts/system/env.py39
-rw-r--r--lib/ansible/module_utils/facts/system/fips.py39
-rw-r--r--lib/ansible/module_utils/facts/system/loadavg.py31
-rw-r--r--lib/ansible/module_utils/facts/system/local.py113
-rw-r--r--lib/ansible/module_utils/facts/system/lsb.py108
-rw-r--r--lib/ansible/module_utils/facts/system/pkg_mgr.py165
-rw-r--r--lib/ansible/module_utils/facts/system/platform.py99
-rw-r--r--lib/ansible/module_utils/facts/system/python.py62
-rw-r--r--lib/ansible/module_utils/facts/system/selinux.py93
-rw-r--r--lib/ansible/module_utils/facts/system/service_mgr.py152
-rw-r--r--lib/ansible/module_utils/facts/system/ssh_pub_keys.py56
-rw-r--r--lib/ansible/module_utils/facts/system/user.py55
20 files changed, 2109 insertions, 0 deletions
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
--- /dev/null
+++ b/lib/ansible/module_utils/facts/system/__init__.py
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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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