summaryrefslogtreecommitdiffstats
path: root/lib/ansible/module_utils/facts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/module_utils/facts')
-rw-r--r--lib/ansible/module_utils/facts/__init__.py34
-rw-r--r--lib/ansible/module_utils/facts/ansible_collector.py158
-rw-r--r--lib/ansible/module_utils/facts/collector.py402
-rw-r--r--lib/ansible/module_utils/facts/compat.py87
-rw-r--r--lib/ansible/module_utils/facts/default_collectors.py177
-rw-r--r--lib/ansible/module_utils/facts/hardware/__init__.py0
-rw-r--r--lib/ansible/module_utils/facts/hardware/aix.py266
-rw-r--r--lib/ansible/module_utils/facts/hardware/base.py68
-rw-r--r--lib/ansible/module_utils/facts/hardware/darwin.py159
-rw-r--r--lib/ansible/module_utils/facts/hardware/dragonfly.py26
-rw-r--r--lib/ansible/module_utils/facts/hardware/freebsd.py241
-rw-r--r--lib/ansible/module_utils/facts/hardware/hpux.py165
-rw-r--r--lib/ansible/module_utils/facts/hardware/hurd.py53
-rw-r--r--lib/ansible/module_utils/facts/hardware/linux.py869
-rw-r--r--lib/ansible/module_utils/facts/hardware/netbsd.py184
-rw-r--r--lib/ansible/module_utils/facts/hardware/openbsd.py184
-rw-r--r--lib/ansible/module_utils/facts/hardware/sunos.py286
-rw-r--r--lib/ansible/module_utils/facts/namespace.py51
-rw-r--r--lib/ansible/module_utils/facts/network/__init__.py0
-rw-r--r--lib/ansible/module_utils/facts/network/aix.py145
-rw-r--r--lib/ansible/module_utils/facts/network/base.py72
-rw-r--r--lib/ansible/module_utils/facts/network/darwin.py49
-rw-r--r--lib/ansible/module_utils/facts/network/dragonfly.py33
-rw-r--r--lib/ansible/module_utils/facts/network/fc_wwn.py111
-rw-r--r--lib/ansible/module_utils/facts/network/freebsd.py33
-rw-r--r--lib/ansible/module_utils/facts/network/generic_bsd.py321
-rw-r--r--lib/ansible/module_utils/facts/network/hpux.py82
-rw-r--r--lib/ansible/module_utils/facts/network/hurd.py87
-rw-r--r--lib/ansible/module_utils/facts/network/iscsi.py115
-rw-r--r--lib/ansible/module_utils/facts/network/linux.py327
-rw-r--r--lib/ansible/module_utils/facts/network/netbsd.py48
-rw-r--r--lib/ansible/module_utils/facts/network/nvme.py57
-rw-r--r--lib/ansible/module_utils/facts/network/openbsd.py42
-rw-r--r--lib/ansible/module_utils/facts/network/sunos.py116
-rw-r--r--lib/ansible/module_utils/facts/other/__init__.py0
-rw-r--r--lib/ansible/module_utils/facts/other/facter.py87
-rw-r--r--lib/ansible/module_utils/facts/other/ohai.py74
-rw-r--r--lib/ansible/module_utils/facts/packages.py86
-rw-r--r--lib/ansible/module_utils/facts/sysctl.py62
-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
-rw-r--r--lib/ansible/module_utils/facts/timeout.py70
-rw-r--r--lib/ansible/module_utils/facts/utils.py102
-rw-r--r--lib/ansible/module_utils/facts/virtual/__init__.py0
-rw-r--r--lib/ansible/module_utils/facts/virtual/base.py80
-rw-r--r--lib/ansible/module_utils/facts/virtual/dragonfly.py25
-rw-r--r--lib/ansible/module_utils/facts/virtual/freebsd.py79
-rw-r--r--lib/ansible/module_utils/facts/virtual/hpux.py72
-rw-r--r--lib/ansible/module_utils/facts/virtual/linux.py405
-rw-r--r--lib/ansible/module_utils/facts/virtual/netbsd.py73
-rw-r--r--lib/ansible/module_utils/facts/virtual/openbsd.py74
-rw-r--r--lib/ansible/module_utils/facts/virtual/sunos.py139
-rw-r--r--lib/ansible/module_utils/facts/virtual/sysctl.py112
71 files changed, 8697 insertions, 0 deletions
diff --git a/lib/ansible/module_utils/facts/__init__.py b/lib/ansible/module_utils/facts/__init__.py
new file mode 100644
index 0000000..96ab778
--- /dev/null
+++ b/lib/ansible/module_utils/facts/__init__.py
@@ -0,0 +1,34 @@
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# (c) 2017 Red Hat Inc.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+# import from the compat api because 2.0-2.3 had a module_utils.facts.ansible_facts
+# and get_all_facts in top level namespace
+from ansible.module_utils.facts.compat import ansible_facts, get_all_facts # noqa
diff --git a/lib/ansible/module_utils/facts/ansible_collector.py b/lib/ansible/module_utils/facts/ansible_collector.py
new file mode 100644
index 0000000..e9bafe2
--- /dev/null
+++ b/lib/ansible/module_utils/facts/ansible_collector.py
@@ -0,0 +1,158 @@
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# (c) 2017 Red Hat Inc.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import fnmatch
+import sys
+
+import ansible.module_utils.compat.typing as t
+
+from ansible.module_utils.facts import timeout
+from ansible.module_utils.facts import collector
+from ansible.module_utils.common.collections import is_string
+
+
+class AnsibleFactCollector(collector.BaseFactCollector):
+ '''A FactCollector that returns results under 'ansible_facts' top level key.
+
+ If a namespace if provided, facts will be collected under that namespace.
+ For ex, a ansible.module_utils.facts.namespace.PrefixFactNamespace(prefix='ansible_')
+
+ Has a 'from_gather_subset() constructor that populates collectors based on a
+ gather_subset specifier.'''
+
+ def __init__(self, collectors=None, namespace=None, filter_spec=None):
+
+ super(AnsibleFactCollector, self).__init__(collectors=collectors,
+ namespace=namespace)
+
+ self.filter_spec = filter_spec
+
+ def _filter(self, facts_dict, filter_spec):
+ # assume filter_spec='' or filter_spec=[] is equivalent to filter_spec='*'
+ if not filter_spec or filter_spec == '*':
+ return facts_dict
+
+ if is_string(filter_spec):
+ filter_spec = [filter_spec]
+
+ found = []
+ for f in filter_spec:
+ for x, y in facts_dict.items():
+ if not f or fnmatch.fnmatch(x, f):
+ found.append((x, y))
+ elif not f.startswith(('ansible_', 'facter', 'ohai')):
+ # try to match with ansible_ prefix added when non empty
+ g = 'ansible_%s' % f
+ if fnmatch.fnmatch(x, g):
+ found.append((x, y))
+ return found
+
+ def collect(self, module=None, collected_facts=None):
+ collected_facts = collected_facts or {}
+
+ facts_dict = {}
+
+ for collector_obj in self.collectors:
+ info_dict = {}
+
+ try:
+
+ # Note: this collects with namespaces, so collected_facts also includes namespaces
+ info_dict = collector_obj.collect_with_namespace(module=module,
+ collected_facts=collected_facts)
+ except Exception as e:
+ sys.stderr.write(repr(e))
+ sys.stderr.write('\n')
+
+ # shallow copy of the new facts to pass to each collector in collected_facts so facts
+ # can reference other facts they depend on.
+ collected_facts.update(info_dict.copy())
+
+ # NOTE: If we want complicated fact dict merging, this is where it would hook in
+ facts_dict.update(self._filter(info_dict, self.filter_spec))
+
+ return facts_dict
+
+
+class CollectorMetaDataCollector(collector.BaseFactCollector):
+ '''Collector that provides a facts with the gather_subset metadata.'''
+
+ name = 'gather_subset'
+ _fact_ids = set() # type: t.Set[str]
+
+ def __init__(self, collectors=None, namespace=None, gather_subset=None, module_setup=None):
+ super(CollectorMetaDataCollector, self).__init__(collectors, namespace)
+ self.gather_subset = gather_subset
+ self.module_setup = module_setup
+
+ def collect(self, module=None, collected_facts=None):
+ meta_facts = {'gather_subset': self.gather_subset}
+ if self.module_setup:
+ meta_facts['module_setup'] = self.module_setup
+ return meta_facts
+
+
+def get_ansible_collector(all_collector_classes,
+ namespace=None,
+ filter_spec=None,
+ gather_subset=None,
+ gather_timeout=None,
+ minimal_gather_subset=None):
+
+ filter_spec = filter_spec or []
+ gather_subset = gather_subset or ['all']
+ gather_timeout = gather_timeout or timeout.DEFAULT_GATHER_TIMEOUT
+ minimal_gather_subset = minimal_gather_subset or frozenset()
+
+ collector_classes = \
+ collector.collector_classes_from_gather_subset(
+ all_collector_classes=all_collector_classes,
+ minimal_gather_subset=minimal_gather_subset,
+ gather_subset=gather_subset,
+ gather_timeout=gather_timeout)
+
+ collectors = []
+ for collector_class in collector_classes:
+ collector_obj = collector_class(namespace=namespace)
+ collectors.append(collector_obj)
+
+ # Add a collector that knows what gather_subset we used so it it can provide a fact
+ collector_meta_data_collector = \
+ CollectorMetaDataCollector(gather_subset=gather_subset,
+ module_setup=True)
+ collectors.append(collector_meta_data_collector)
+
+ fact_collector = \
+ AnsibleFactCollector(collectors=collectors,
+ filter_spec=filter_spec,
+ namespace=namespace)
+
+ return fact_collector
diff --git a/lib/ansible/module_utils/facts/collector.py b/lib/ansible/module_utils/facts/collector.py
new file mode 100644
index 0000000..ac52fe8
--- /dev/null
+++ b/lib/ansible/module_utils/facts/collector.py
@@ -0,0 +1,402 @@
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# (c) 2017 Red Hat Inc.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from collections import defaultdict
+
+import platform
+
+import ansible.module_utils.compat.typing as t
+
+from ansible.module_utils.facts import timeout
+
+
+class CycleFoundInFactDeps(Exception):
+ '''Indicates there is a cycle in fact collector deps
+
+ If collector-B requires collector-A, and collector-A requires
+ collector-B, that is a cycle. In that case, there is no ordering
+ that will satisfy B before A and A and before B. That will cause this
+ error to be raised.
+ '''
+ pass
+
+
+class UnresolvedFactDep(ValueError):
+ pass
+
+
+class CollectorNotFoundError(KeyError):
+ pass
+
+
+class BaseFactCollector:
+ _fact_ids = set() # type: t.Set[str]
+
+ _platform = 'Generic'
+ name = None # type: str | None
+ required_facts = set() # type: t.Set[str]
+
+ def __init__(self, collectors=None, namespace=None):
+ '''Base class for things that collect facts.
+
+ 'collectors' is an optional list of other FactCollectors for composing.'''
+ self.collectors = collectors or []
+
+ # self.namespace is a object with a 'transform' method that transforms
+ # the name to indicate the namespace (ie, adds a prefix or suffix).
+ self.namespace = namespace
+
+ self.fact_ids = set([self.name])
+ self.fact_ids.update(self._fact_ids)
+
+ @classmethod
+ def platform_match(cls, platform_info):
+ if platform_info.get('system', None) == cls._platform:
+ return cls
+ return None
+
+ def _transform_name(self, key_name):
+ if self.namespace:
+ return self.namespace.transform(key_name)
+ return key_name
+
+ def _transform_dict_keys(self, fact_dict):
+ '''update a dicts keys to use new names as transformed by self._transform_name'''
+
+ for old_key in list(fact_dict.keys()):
+ new_key = self._transform_name(old_key)
+ # pop the item by old_key and replace it using new_key
+ fact_dict[new_key] = fact_dict.pop(old_key)
+ return fact_dict
+
+ # TODO/MAYBE: rename to 'collect' and add 'collect_without_namespace'
+ def collect_with_namespace(self, module=None, collected_facts=None):
+ # collect, then transform the key names if needed
+ facts_dict = self.collect(module=module, collected_facts=collected_facts)
+ if self.namespace:
+ facts_dict = self._transform_dict_keys(facts_dict)
+ return facts_dict
+
+ def collect(self, module=None, collected_facts=None):
+ '''do the fact collection
+
+ 'collected_facts' is a object (a dict, likely) that holds all previously
+ facts. This is intended to be used if a FactCollector needs to reference
+ another fact (for ex, the system arch) and should not be modified (usually).
+
+ Returns a dict of facts.
+
+ '''
+ facts_dict = {}
+ return facts_dict
+
+
+def get_collector_names(valid_subsets=None,
+ minimal_gather_subset=None,
+ gather_subset=None,
+ aliases_map=None,
+ platform_info=None):
+ '''return a set of FactCollector names based on gather_subset spec.
+
+ gather_subset is a spec describing which facts to gather.
+ valid_subsets is a frozenset of potential matches for gather_subset ('all', 'network') etc
+ minimal_gather_subsets is a frozenset of matches to always use, even for gather_subset='!all'
+ '''
+
+ # Retrieve module parameters
+ gather_subset = gather_subset or ['all']
+
+ # the list of everything that 'all' expands to
+ valid_subsets = valid_subsets or frozenset()
+
+ # if provided, minimal_gather_subset is always added, even after all negations
+ minimal_gather_subset = minimal_gather_subset or frozenset()
+
+ aliases_map = aliases_map or defaultdict(set)
+
+ # Retrieve all facts elements
+ additional_subsets = set()
+ exclude_subsets = set()
+
+ # total always starts with the min set, then
+ # adds of the additions in gather_subset, then
+ # excludes all of the excludes, then add any explicitly
+ # requested subsets.
+ gather_subset_with_min = ['min']
+ gather_subset_with_min.extend(gather_subset)
+
+ # subsets we mention in gather_subset explicitly, except for 'all'/'min'
+ explicitly_added = set()
+
+ for subset in gather_subset_with_min:
+ subset_id = subset
+ if subset_id == 'min':
+ additional_subsets.update(minimal_gather_subset)
+ continue
+ if subset_id == 'all':
+ additional_subsets.update(valid_subsets)
+ continue
+ if subset_id.startswith('!'):
+ subset = subset[1:]
+ if subset == 'min':
+ exclude_subsets.update(minimal_gather_subset)
+ continue
+ if subset == 'all':
+ exclude_subsets.update(valid_subsets - minimal_gather_subset)
+ continue
+ exclude = True
+ else:
+ exclude = False
+
+ if exclude:
+ # include 'devices', 'dmi' etc for '!hardware'
+ exclude_subsets.update(aliases_map.get(subset, set()))
+ exclude_subsets.add(subset)
+ else:
+ # NOTE: this only considers adding an unknown gather subsetup an error. Asking to
+ # exclude an unknown gather subset is ignored.
+ if subset_id not in valid_subsets:
+ raise TypeError("Bad subset '%s' given to Ansible. gather_subset options allowed: all, %s" %
+ (subset, ", ".join(sorted(valid_subsets))))
+
+ explicitly_added.add(subset)
+ additional_subsets.add(subset)
+
+ if not additional_subsets:
+ additional_subsets.update(valid_subsets)
+
+ additional_subsets.difference_update(exclude_subsets - explicitly_added)
+
+ return additional_subsets
+
+
+def find_collectors_for_platform(all_collector_classes, compat_platforms):
+ found_collectors = set()
+ found_collectors_names = set()
+
+ # start from specific platform, then try generic
+ for compat_platform in compat_platforms:
+ platform_match = None
+ for all_collector_class in all_collector_classes:
+
+ # ask the class if it is compatible with the platform info
+ platform_match = all_collector_class.platform_match(compat_platform)
+
+ if not platform_match:
+ continue
+
+ primary_name = all_collector_class.name
+
+ if primary_name not in found_collectors_names:
+ found_collectors.add(all_collector_class)
+ found_collectors_names.add(all_collector_class.name)
+
+ return found_collectors
+
+
+def build_fact_id_to_collector_map(collectors_for_platform):
+ fact_id_to_collector_map = defaultdict(list)
+ aliases_map = defaultdict(set)
+
+ for collector_class in collectors_for_platform:
+ primary_name = collector_class.name
+
+ fact_id_to_collector_map[primary_name].append(collector_class)
+
+ for fact_id in collector_class._fact_ids:
+ fact_id_to_collector_map[fact_id].append(collector_class)
+ aliases_map[primary_name].add(fact_id)
+
+ return fact_id_to_collector_map, aliases_map
+
+
+def select_collector_classes(collector_names, all_fact_subsets):
+ seen_collector_classes = set()
+
+ selected_collector_classes = []
+
+ for collector_name in collector_names:
+ collector_classes = all_fact_subsets.get(collector_name, [])
+ for collector_class in collector_classes:
+ if collector_class not in seen_collector_classes:
+ selected_collector_classes.append(collector_class)
+ seen_collector_classes.add(collector_class)
+
+ return selected_collector_classes
+
+
+def _get_requires_by_collector_name(collector_name, all_fact_subsets):
+ required_facts = set()
+
+ try:
+ collector_classes = all_fact_subsets[collector_name]
+ except KeyError:
+ raise CollectorNotFoundError('Fact collector "%s" not found' % collector_name)
+ for collector_class in collector_classes:
+ required_facts.update(collector_class.required_facts)
+ return required_facts
+
+
+def find_unresolved_requires(collector_names, all_fact_subsets):
+ '''Find any collector names that have unresolved requires
+
+ Returns a list of collector names that correspond to collector
+ classes whose .requires_facts() are not in collector_names.
+ '''
+ unresolved = set()
+
+ for collector_name in collector_names:
+ required_facts = _get_requires_by_collector_name(collector_name, all_fact_subsets)
+ for required_fact in required_facts:
+ if required_fact not in collector_names:
+ unresolved.add(required_fact)
+
+ return unresolved
+
+
+def resolve_requires(unresolved_requires, all_fact_subsets):
+ new_names = set()
+ failed = []
+ for unresolved in unresolved_requires:
+ if unresolved in all_fact_subsets:
+ new_names.add(unresolved)
+ else:
+ failed.append(unresolved)
+
+ if failed:
+ raise UnresolvedFactDep('unresolved fact dep %s' % ','.join(failed))
+ return new_names
+
+
+def build_dep_data(collector_names, all_fact_subsets):
+ dep_map = defaultdict(set)
+ for collector_name in collector_names:
+ collector_deps = set()
+ for collector in all_fact_subsets[collector_name]:
+ for dep in collector.required_facts:
+ collector_deps.add(dep)
+ dep_map[collector_name] = collector_deps
+ return dep_map
+
+
+def tsort(dep_map):
+ sorted_list = []
+
+ unsorted_map = dep_map.copy()
+
+ while unsorted_map:
+ acyclic = False
+ for node, edges in list(unsorted_map.items()):
+ for edge in edges:
+ if edge in unsorted_map:
+ break
+ else:
+ acyclic = True
+ del unsorted_map[node]
+ sorted_list.append((node, edges))
+
+ if not acyclic:
+ raise CycleFoundInFactDeps('Unable to tsort deps, there was a cycle in the graph. sorted=%s' % sorted_list)
+
+ return sorted_list
+
+
+def _solve_deps(collector_names, all_fact_subsets):
+ unresolved = collector_names.copy()
+ solutions = collector_names.copy()
+
+ while True:
+ unresolved = find_unresolved_requires(solutions, all_fact_subsets)
+ if unresolved == set():
+ break
+
+ new_names = resolve_requires(unresolved, all_fact_subsets)
+ solutions.update(new_names)
+
+ return solutions
+
+
+def collector_classes_from_gather_subset(all_collector_classes=None,
+ valid_subsets=None,
+ minimal_gather_subset=None,
+ gather_subset=None,
+ gather_timeout=None,
+ platform_info=None):
+ '''return a list of collector classes that match the args'''
+
+ # use gather_name etc to get the list of collectors
+
+ all_collector_classes = all_collector_classes or []
+
+ minimal_gather_subset = minimal_gather_subset or frozenset()
+
+ platform_info = platform_info or {'system': platform.system()}
+
+ gather_timeout = gather_timeout or timeout.DEFAULT_GATHER_TIMEOUT
+
+ # tweak the modules GATHER_TIMEOUT
+ timeout.GATHER_TIMEOUT = gather_timeout
+
+ valid_subsets = valid_subsets or frozenset()
+
+ # maps alias names like 'hardware' to the list of names that are part of hardware
+ # like 'devices' and 'dmi'
+ aliases_map = defaultdict(set)
+
+ compat_platforms = [platform_info, {'system': 'Generic'}]
+
+ collectors_for_platform = find_collectors_for_platform(all_collector_classes, compat_platforms)
+
+ # all_facts_subsets maps the subset name ('hardware') to the class that provides it.
+
+ # TODO: name collisions here? are there facts with the same name as a gather_subset (all, network, hardware, virtual, ohai, facter)
+ all_fact_subsets, aliases_map = build_fact_id_to_collector_map(collectors_for_platform)
+
+ all_valid_subsets = frozenset(all_fact_subsets.keys())
+
+ # expand any fact_id/collectorname/gather_subset term ('all', 'env', etc) to the list of names that represents
+ collector_names = get_collector_names(valid_subsets=all_valid_subsets,
+ minimal_gather_subset=minimal_gather_subset,
+ gather_subset=gather_subset,
+ aliases_map=aliases_map,
+ platform_info=platform_info)
+
+ complete_collector_names = _solve_deps(collector_names, all_fact_subsets)
+
+ dep_map = build_dep_data(complete_collector_names, all_fact_subsets)
+
+ ordered_deps = tsort(dep_map)
+ ordered_collector_names = [x[0] for x in ordered_deps]
+
+ selected_collector_classes = select_collector_classes(ordered_collector_names,
+ all_fact_subsets)
+
+ return selected_collector_classes
diff --git a/lib/ansible/module_utils/facts/compat.py b/lib/ansible/module_utils/facts/compat.py
new file mode 100644
index 0000000..a69fee3
--- /dev/null
+++ b/lib/ansible/module_utils/facts/compat.py
@@ -0,0 +1,87 @@
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# (c) 2017 Red Hat Inc.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils.facts.namespace import PrefixFactNamespace
+from ansible.module_utils.facts import default_collectors
+from ansible.module_utils.facts import ansible_collector
+
+
+def get_all_facts(module):
+ '''compat api for ansible 2.2/2.3 module_utils.facts.get_all_facts method
+
+ Expects module to be an instance of AnsibleModule, with a 'gather_subset' param.
+
+ returns a dict mapping the bare fact name ('default_ipv4' with no 'ansible_' namespace) to
+ the fact value.'''
+
+ gather_subset = module.params['gather_subset']
+ return ansible_facts(module, gather_subset=gather_subset)
+
+
+def ansible_facts(module, gather_subset=None):
+ '''Compat api for ansible 2.0/2.2/2.3 module_utils.facts.ansible_facts method
+
+ 2.3/2.3 expects a gather_subset arg.
+ 2.0/2.1 does not except a gather_subset arg
+
+ So make gather_subsets an optional arg, defaulting to configured DEFAULT_GATHER_TIMEOUT
+
+ 'module' should be an instance of an AnsibleModule.
+
+ returns a dict mapping the bare fact name ('default_ipv4' with no 'ansible_' namespace) to
+ the fact value.
+ '''
+
+ gather_subset = gather_subset or module.params.get('gather_subset', ['all'])
+ gather_timeout = module.params.get('gather_timeout', 10)
+ filter_spec = module.params.get('filter', '*')
+
+ minimal_gather_subset = frozenset(['apparmor', 'caps', 'cmdline', 'date_time',
+ 'distribution', 'dns', 'env', 'fips', 'local',
+ 'lsb', 'pkg_mgr', 'platform', 'python', 'selinux',
+ 'service_mgr', 'ssh_pub_keys', 'user'])
+
+ all_collector_classes = default_collectors.collectors
+
+ # don't add a prefix
+ namespace = PrefixFactNamespace(namespace_name='ansible', prefix='')
+
+ fact_collector = \
+ ansible_collector.get_ansible_collector(all_collector_classes=all_collector_classes,
+ namespace=namespace,
+ filter_spec=filter_spec,
+ gather_subset=gather_subset,
+ gather_timeout=gather_timeout,
+ minimal_gather_subset=minimal_gather_subset)
+
+ facts_dict = fact_collector.collect(module=module)
+
+ return facts_dict
diff --git a/lib/ansible/module_utils/facts/default_collectors.py b/lib/ansible/module_utils/facts/default_collectors.py
new file mode 100644
index 0000000..cf0ef23
--- /dev/null
+++ b/lib/ansible/module_utils/facts/default_collectors.py
@@ -0,0 +1,177 @@
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# (c) 2017 Red Hat Inc.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+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
+
+from ansible.module_utils.facts.other.facter import FacterFactCollector
+from ansible.module_utils.facts.other.ohai import OhaiFactCollector
+
+from ansible.module_utils.facts.system.apparmor import ApparmorFactCollector
+from ansible.module_utils.facts.system.caps import SystemCapabilitiesFactCollector
+from ansible.module_utils.facts.system.chroot import ChrootFactCollector
+from ansible.module_utils.facts.system.cmdline import CmdLineFactCollector
+from ansible.module_utils.facts.system.distribution import DistributionFactCollector
+from ansible.module_utils.facts.system.date_time import DateTimeFactCollector
+from ansible.module_utils.facts.system.env import EnvFactCollector
+from ansible.module_utils.facts.system.dns import DnsFactCollector
+from ansible.module_utils.facts.system.fips import FipsFactCollector
+from ansible.module_utils.facts.system.loadavg import LoadAvgFactCollector
+from ansible.module_utils.facts.system.local import LocalFactCollector
+from ansible.module_utils.facts.system.lsb import LSBFactCollector
+from ansible.module_utils.facts.system.pkg_mgr import PkgMgrFactCollector
+from ansible.module_utils.facts.system.pkg_mgr import OpenBSDPkgMgrFactCollector
+from ansible.module_utils.facts.system.platform import PlatformFactCollector
+from ansible.module_utils.facts.system.python import PythonFactCollector
+from ansible.module_utils.facts.system.selinux import SelinuxFactCollector
+from ansible.module_utils.facts.system.service_mgr import ServiceMgrFactCollector
+from ansible.module_utils.facts.system.ssh_pub_keys import SshPubKeyFactCollector
+from ansible.module_utils.facts.system.user import UserFactCollector
+
+from ansible.module_utils.facts.hardware.base import HardwareCollector
+from ansible.module_utils.facts.hardware.aix import AIXHardwareCollector
+from ansible.module_utils.facts.hardware.darwin import DarwinHardwareCollector
+from ansible.module_utils.facts.hardware.dragonfly import DragonFlyHardwareCollector
+from ansible.module_utils.facts.hardware.freebsd import FreeBSDHardwareCollector
+from ansible.module_utils.facts.hardware.hpux import HPUXHardwareCollector
+from ansible.module_utils.facts.hardware.hurd import HurdHardwareCollector
+from ansible.module_utils.facts.hardware.linux import LinuxHardwareCollector
+from ansible.module_utils.facts.hardware.netbsd import NetBSDHardwareCollector
+from ansible.module_utils.facts.hardware.openbsd import OpenBSDHardwareCollector
+from ansible.module_utils.facts.hardware.sunos import SunOSHardwareCollector
+
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.network.aix import AIXNetworkCollector
+from ansible.module_utils.facts.network.darwin import DarwinNetworkCollector
+from ansible.module_utils.facts.network.dragonfly import DragonFlyNetworkCollector
+from ansible.module_utils.facts.network.fc_wwn import FcWwnInitiatorFactCollector
+from ansible.module_utils.facts.network.freebsd import FreeBSDNetworkCollector
+from ansible.module_utils.facts.network.hpux import HPUXNetworkCollector
+from ansible.module_utils.facts.network.hurd import HurdNetworkCollector
+from ansible.module_utils.facts.network.linux import LinuxNetworkCollector
+from ansible.module_utils.facts.network.iscsi import IscsiInitiatorNetworkCollector
+from ansible.module_utils.facts.network.nvme import NvmeInitiatorNetworkCollector
+from ansible.module_utils.facts.network.netbsd import NetBSDNetworkCollector
+from ansible.module_utils.facts.network.openbsd import OpenBSDNetworkCollector
+from ansible.module_utils.facts.network.sunos import SunOSNetworkCollector
+
+from ansible.module_utils.facts.virtual.base import VirtualCollector
+from ansible.module_utils.facts.virtual.dragonfly import DragonFlyVirtualCollector
+from ansible.module_utils.facts.virtual.freebsd import FreeBSDVirtualCollector
+from ansible.module_utils.facts.virtual.hpux import HPUXVirtualCollector
+from ansible.module_utils.facts.virtual.linux import LinuxVirtualCollector
+from ansible.module_utils.facts.virtual.netbsd import NetBSDVirtualCollector
+from ansible.module_utils.facts.virtual.openbsd import OpenBSDVirtualCollector
+from ansible.module_utils.facts.virtual.sunos import SunOSVirtualCollector
+
+# these should always be first due to most other facts depending on them
+_base = [
+ PlatformFactCollector,
+ DistributionFactCollector,
+ LSBFactCollector
+] # type: t.List[t.Type[BaseFactCollector]]
+
+# These restrict what is possible in others
+_restrictive = [
+ SelinuxFactCollector,
+ ApparmorFactCollector,
+ ChrootFactCollector,
+ FipsFactCollector
+] # type: t.List[t.Type[BaseFactCollector]]
+
+# general info, not required but probably useful for other facts
+_general = [
+ PythonFactCollector,
+ SystemCapabilitiesFactCollector,
+ PkgMgrFactCollector,
+ OpenBSDPkgMgrFactCollector,
+ ServiceMgrFactCollector,
+ CmdLineFactCollector,
+ DateTimeFactCollector,
+ EnvFactCollector,
+ LoadAvgFactCollector,
+ SshPubKeyFactCollector,
+ UserFactCollector
+] # type: t.List[t.Type[BaseFactCollector]]
+
+# virtual, this might also limit hardware/networking
+_virtual = [
+ VirtualCollector,
+ DragonFlyVirtualCollector,
+ FreeBSDVirtualCollector,
+ LinuxVirtualCollector,
+ OpenBSDVirtualCollector,
+ NetBSDVirtualCollector,
+ SunOSVirtualCollector,
+ HPUXVirtualCollector
+] # type: t.List[t.Type[BaseFactCollector]]
+
+_hardware = [
+ HardwareCollector,
+ AIXHardwareCollector,
+ DarwinHardwareCollector,
+ DragonFlyHardwareCollector,
+ FreeBSDHardwareCollector,
+ HPUXHardwareCollector,
+ HurdHardwareCollector,
+ LinuxHardwareCollector,
+ NetBSDHardwareCollector,
+ OpenBSDHardwareCollector,
+ SunOSHardwareCollector
+] # type: t.List[t.Type[BaseFactCollector]]
+
+_network = [
+ DnsFactCollector,
+ FcWwnInitiatorFactCollector,
+ NetworkCollector,
+ AIXNetworkCollector,
+ DarwinNetworkCollector,
+ DragonFlyNetworkCollector,
+ FreeBSDNetworkCollector,
+ HPUXNetworkCollector,
+ HurdNetworkCollector,
+ IscsiInitiatorNetworkCollector,
+ NvmeInitiatorNetworkCollector,
+ LinuxNetworkCollector,
+ NetBSDNetworkCollector,
+ OpenBSDNetworkCollector,
+ SunOSNetworkCollector
+] # type: t.List[t.Type[BaseFactCollector]]
+
+# other fact sources
+_extra_facts = [
+ LocalFactCollector,
+ FacterFactCollector,
+ OhaiFactCollector
+] # type: t.List[t.Type[BaseFactCollector]]
+
+# TODO: make config driven
+collectors = _base + _restrictive + _general + _virtual + _hardware + _network + _extra_facts
diff --git a/lib/ansible/module_utils/facts/hardware/__init__.py b/lib/ansible/module_utils/facts/hardware/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/ansible/module_utils/facts/hardware/__init__.py
diff --git a/lib/ansible/module_utils/facts/hardware/aix.py b/lib/ansible/module_utils/facts/hardware/aix.py
new file mode 100644
index 0000000..dc37394
--- /dev/null
+++ b/lib/ansible/module_utils/facts/hardware/aix.py
@@ -0,0 +1,266 @@
+# 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
+
+from ansible.module_utils.facts.hardware.base import Hardware, HardwareCollector
+from ansible.module_utils.facts.utils import get_mount_size
+
+
+class AIXHardware(Hardware):
+ """
+ AIX-specific subclass of Hardware. Defines memory and CPU facts:
+ - memfree_mb
+ - memtotal_mb
+ - swapfree_mb
+ - swaptotal_mb
+ - processor (a list)
+ - processor_count
+ - processor_cores
+ - processor_threads_per_core
+ - processor_vcpus
+ """
+ platform = 'AIX'
+
+ def populate(self, collected_facts=None):
+ hardware_facts = {}
+
+ cpu_facts = self.get_cpu_facts()
+ memory_facts = self.get_memory_facts()
+ dmi_facts = self.get_dmi_facts()
+ vgs_facts = self.get_vgs_facts()
+ mount_facts = self.get_mount_facts()
+ devices_facts = self.get_device_facts()
+
+ hardware_facts.update(cpu_facts)
+ hardware_facts.update(memory_facts)
+ hardware_facts.update(dmi_facts)
+ hardware_facts.update(vgs_facts)
+ hardware_facts.update(mount_facts)
+ hardware_facts.update(devices_facts)
+
+ return hardware_facts
+
+ def get_cpu_facts(self):
+ cpu_facts = {}
+ cpu_facts['processor'] = []
+
+ # FIXME: not clear how to detect multi-sockets
+ cpu_facts['processor_count'] = 1
+ rc, out, err = self.module.run_command(
+ "/usr/sbin/lsdev -Cc processor"
+ )
+ if out:
+ i = 0
+ for line in out.splitlines():
+
+ if 'Available' in line:
+ if i == 0:
+ data = line.split(' ')
+ cpudev = data[0]
+
+ i += 1
+ cpu_facts['processor_cores'] = int(i)
+
+ rc, out, err = self.module.run_command(
+ "/usr/sbin/lsattr -El " + cpudev + " -a type"
+ )
+
+ data = out.split(' ')
+ cpu_facts['processor'] = [data[1]]
+
+ cpu_facts['processor_threads_per_core'] = 1
+ rc, out, err = self.module.run_command(
+ "/usr/sbin/lsattr -El " + cpudev + " -a smt_threads"
+ )
+ if out:
+ data = out.split(' ')
+ cpu_facts['processor_threads_per_core'] = int(data[1])
+ cpu_facts['processor_vcpus'] = (
+ cpu_facts['processor_cores'] * cpu_facts['processor_threads_per_core']
+ )
+
+ return cpu_facts
+
+ def get_memory_facts(self):
+ memory_facts = {}
+ pagesize = 4096
+ rc, out, err = self.module.run_command("/usr/bin/vmstat -v")
+ for line in out.splitlines():
+ data = line.split()
+ if 'memory pages' in line:
+ pagecount = int(data[0])
+ if 'free pages' in line:
+ freecount = int(data[0])
+ memory_facts['memtotal_mb'] = pagesize * pagecount // 1024 // 1024
+ memory_facts['memfree_mb'] = pagesize * freecount // 1024 // 1024
+ # Get swapinfo. swapinfo output looks like:
+ # Device 1M-blocks Used Avail Capacity
+ # /dev/ada0p3 314368 0 314368 0%
+ #
+ rc, out, err = self.module.run_command("/usr/sbin/lsps -s")
+ if out:
+ lines = out.splitlines()
+ data = lines[1].split()
+ swaptotal_mb = int(data[0].rstrip('MB'))
+ percused = int(data[1].rstrip('%'))
+ memory_facts['swaptotal_mb'] = swaptotal_mb
+ memory_facts['swapfree_mb'] = int(swaptotal_mb * (100 - percused) / 100)
+
+ return memory_facts
+
+ def get_dmi_facts(self):
+ dmi_facts = {}
+
+ rc, out, err = self.module.run_command("/usr/sbin/lsattr -El sys0 -a fwversion")
+ data = out.split()
+ dmi_facts['firmware_version'] = data[1].strip('IBM,')
+ lsconf_path = self.module.get_bin_path("lsconf")
+ if lsconf_path:
+ rc, out, err = self.module.run_command(lsconf_path)
+ if rc == 0 and out:
+ for line in out.splitlines():
+ data = line.split(':')
+ if 'Machine Serial Number' in line:
+ dmi_facts['product_serial'] = data[1].strip()
+ if 'LPAR Info' in line:
+ dmi_facts['lpar_info'] = data[1].strip()
+ if 'System Model' in line:
+ dmi_facts['product_name'] = data[1].strip()
+ return dmi_facts
+
+ def get_vgs_facts(self):
+ """
+ Get vg and pv Facts
+ rootvg:
+ PV_NAME PV STATE TOTAL PPs FREE PPs FREE DISTRIBUTION
+ hdisk0 active 546 0 00..00..00..00..00
+ hdisk1 active 546 113 00..00..00..21..92
+ realsyncvg:
+ PV_NAME PV STATE TOTAL PPs FREE PPs FREE DISTRIBUTION
+ hdisk74 active 1999 6 00..00..00..00..06
+ testvg:
+ PV_NAME PV STATE TOTAL PPs FREE PPs FREE DISTRIBUTION
+ hdisk105 active 999 838 200..39..199..200..200
+ hdisk106 active 999 599 200..00..00..199..200
+ """
+
+ vgs_facts = {}
+ lsvg_path = self.module.get_bin_path("lsvg")
+ xargs_path = self.module.get_bin_path("xargs")
+ cmd = "%s -o | %s %s -p" % (lsvg_path, xargs_path, lsvg_path)
+ if lsvg_path and xargs_path:
+ rc, out, err = self.module.run_command(cmd, use_unsafe_shell=True)
+ if rc == 0 and out:
+ vgs_facts['vgs'] = {}
+ for m in re.finditer(r'(\S+):\n.*FREE DISTRIBUTION(\n(\S+)\s+(\w+)\s+(\d+)\s+(\d+).*)+', out):
+ vgs_facts['vgs'][m.group(1)] = []
+ pp_size = 0
+ cmd = "%s %s" % (lsvg_path, m.group(1))
+ rc, out, err = self.module.run_command(cmd)
+ if rc == 0 and out:
+ pp_size = re.search(r'PP SIZE:\s+(\d+\s+\S+)', out).group(1)
+ for n in re.finditer(r'(\S+)\s+(\w+)\s+(\d+)\s+(\d+).*', m.group(0)):
+ pv_info = {'pv_name': n.group(1),
+ 'pv_state': n.group(2),
+ 'total_pps': n.group(3),
+ 'free_pps': n.group(4),
+ 'pp_size': pp_size
+ }
+ vgs_facts['vgs'][m.group(1)].append(pv_info)
+
+ return vgs_facts
+
+ def get_mount_facts(self):
+ mount_facts = {}
+
+ mount_facts['mounts'] = []
+
+ mounts = []
+
+ # AIX does not have mtab but mount command is only source of info (or to use
+ # api calls to get same info)
+ mount_path = self.module.get_bin_path('mount')
+ rc, mount_out, err = self.module.run_command(mount_path)
+ if mount_out:
+ for line in mount_out.split('\n'):
+ fields = line.split()
+ if len(fields) != 0 and fields[0] != 'node' and fields[0][0] != '-' and re.match('^/.*|^[a-zA-Z].*|^[0-9].*', fields[0]):
+ if re.match('^/', fields[0]):
+ # normal mount
+ mount = fields[1]
+ mount_info = {'mount': mount,
+ 'device': fields[0],
+ 'fstype': fields[2],
+ 'options': fields[6],
+ 'time': '%s %s %s' % (fields[3], fields[4], fields[5])}
+ mount_info.update(get_mount_size(mount))
+ else:
+ # nfs or cifs based mount
+ # in case of nfs if no mount options are provided on command line
+ # add into fields empty string...
+ if len(fields) < 8:
+ fields.append("")
+
+ mount_info = {'mount': fields[2],
+ 'device': '%s:%s' % (fields[0], fields[1]),
+ 'fstype': fields[3],
+ 'options': fields[7],
+ 'time': '%s %s %s' % (fields[4], fields[5], fields[6])}
+
+ mounts.append(mount_info)
+
+ mount_facts['mounts'] = mounts
+
+ return mount_facts
+
+ def get_device_facts(self):
+ device_facts = {}
+ device_facts['devices'] = {}
+
+ lsdev_cmd = self.module.get_bin_path('lsdev', True)
+ lsattr_cmd = self.module.get_bin_path('lsattr', True)
+ rc, out_lsdev, err = self.module.run_command(lsdev_cmd)
+
+ for line in out_lsdev.splitlines():
+ field = line.split()
+
+ device_attrs = {}
+ device_name = field[0]
+ device_state = field[1]
+ device_type = field[2:]
+ lsattr_cmd_args = [lsattr_cmd, '-E', '-l', device_name]
+ rc, out_lsattr, err = self.module.run_command(lsattr_cmd_args)
+ for attr in out_lsattr.splitlines():
+ attr_fields = attr.split()
+ attr_name = attr_fields[0]
+ attr_parameter = attr_fields[1]
+ device_attrs[attr_name] = attr_parameter
+
+ device_facts['devices'][device_name] = {
+ 'state': device_state,
+ 'type': ' '.join(device_type),
+ 'attributes': device_attrs
+ }
+
+ return device_facts
+
+
+class AIXHardwareCollector(HardwareCollector):
+ _platform = 'AIX'
+ _fact_class = AIXHardware
diff --git a/lib/ansible/module_utils/facts/hardware/base.py b/lib/ansible/module_utils/facts/hardware/base.py
new file mode 100644
index 0000000..846bb30
--- /dev/null
+++ b/lib/ansible/module_utils/facts/hardware/base.py
@@ -0,0 +1,68 @@
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# (c) 2017 Red Hat Inc.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+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
+
+
+class Hardware:
+ platform = 'Generic'
+
+ # FIXME: remove load_on_init when we can
+ def __init__(self, module, load_on_init=False):
+ self.module = module
+
+ def populate(self, collected_facts=None):
+ return {}
+
+
+class HardwareCollector(BaseFactCollector):
+ name = 'hardware'
+ _fact_ids = set(['processor',
+ 'processor_cores',
+ 'processor_count',
+ # TODO: mounts isnt exactly hardware
+ 'mounts',
+ 'devices']) # type: t.Set[str]
+ _fact_class = Hardware
+
+ def collect(self, module=None, collected_facts=None):
+ collected_facts = collected_facts or {}
+ if not module:
+ return {}
+
+ # Network munges cached_facts by side effect, so give it a copy
+ facts_obj = self._fact_class(module)
+
+ facts_dict = facts_obj.populate(collected_facts=collected_facts)
+
+ return facts_dict
diff --git a/lib/ansible/module_utils/facts/hardware/darwin.py b/lib/ansible/module_utils/facts/hardware/darwin.py
new file mode 100644
index 0000000..d6a8e11
--- /dev/null
+++ b/lib/ansible/module_utils/facts/hardware/darwin.py
@@ -0,0 +1,159 @@
+# 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 struct
+import time
+
+from ansible.module_utils.common.process import get_bin_path
+from ansible.module_utils.facts.hardware.base import Hardware, HardwareCollector
+from ansible.module_utils.facts.sysctl import get_sysctl
+
+
+class DarwinHardware(Hardware):
+ """
+ Darwin-specific subclass of Hardware. Defines memory and CPU facts:
+ - processor
+ - processor_cores
+ - memtotal_mb
+ - memfree_mb
+ - model
+ - osversion
+ - osrevision
+ - uptime_seconds
+ """
+ platform = 'Darwin'
+
+ def populate(self, collected_facts=None):
+ hardware_facts = {}
+
+ self.sysctl = get_sysctl(self.module, ['hw', 'machdep', 'kern'])
+ mac_facts = self.get_mac_facts()
+ cpu_facts = self.get_cpu_facts()
+ memory_facts = self.get_memory_facts()
+ uptime_facts = self.get_uptime_facts()
+
+ hardware_facts.update(mac_facts)
+ hardware_facts.update(cpu_facts)
+ hardware_facts.update(memory_facts)
+ hardware_facts.update(uptime_facts)
+
+ return hardware_facts
+
+ def get_system_profile(self):
+ rc, out, err = self.module.run_command(["/usr/sbin/system_profiler", "SPHardwareDataType"])
+ if rc != 0:
+ return dict()
+ system_profile = dict()
+ for line in out.splitlines():
+ if ': ' in line:
+ (key, value) = line.split(': ', 1)
+ system_profile[key.strip()] = ' '.join(value.strip().split())
+ return system_profile
+
+ def get_mac_facts(self):
+ mac_facts = {}
+ rc, out, err = self.module.run_command("sysctl hw.model")
+ if rc == 0:
+ mac_facts['model'] = mac_facts['product_name'] = out.splitlines()[-1].split()[1]
+ mac_facts['osversion'] = self.sysctl['kern.osversion']
+ mac_facts['osrevision'] = self.sysctl['kern.osrevision']
+
+ return mac_facts
+
+ def get_cpu_facts(self):
+ cpu_facts = {}
+ if 'machdep.cpu.brand_string' in self.sysctl: # Intel
+ cpu_facts['processor'] = self.sysctl['machdep.cpu.brand_string']
+ cpu_facts['processor_cores'] = self.sysctl['machdep.cpu.core_count']
+ else: # PowerPC
+ system_profile = self.get_system_profile()
+ cpu_facts['processor'] = '%s @ %s' % (system_profile['Processor Name'], system_profile['Processor Speed'])
+ cpu_facts['processor_cores'] = self.sysctl['hw.physicalcpu']
+ cpu_facts['processor_vcpus'] = self.sysctl.get('hw.logicalcpu') or self.sysctl.get('hw.ncpu') or ''
+
+ return cpu_facts
+
+ def get_memory_facts(self):
+ memory_facts = {
+ 'memtotal_mb': int(self.sysctl['hw.memsize']) // 1024 // 1024,
+ 'memfree_mb': 0,
+ }
+
+ total_used = 0
+ page_size = 4096
+ try:
+ vm_stat_command = get_bin_path('vm_stat')
+ except ValueError:
+ return memory_facts
+
+ rc, out, err = self.module.run_command(vm_stat_command)
+ if rc == 0:
+ # Free = Total - (Wired + active + inactive)
+ # Get a generator of tuples from the command output so we can later
+ # turn it into a dictionary
+ memory_stats = (line.rstrip('.').split(':', 1) for line in out.splitlines())
+
+ # Strip extra left spaces from the value
+ memory_stats = dict((k, v.lstrip()) for k, v in memory_stats)
+
+ for k, v in memory_stats.items():
+ try:
+ memory_stats[k] = int(v)
+ except ValueError:
+ # Most values convert cleanly to integer values but if the field does
+ # not convert to an integer, just leave it alone.
+ pass
+
+ if memory_stats.get('Pages wired down'):
+ total_used += memory_stats['Pages wired down'] * page_size
+ if memory_stats.get('Pages active'):
+ total_used += memory_stats['Pages active'] * page_size
+ if memory_stats.get('Pages inactive'):
+ total_used += memory_stats['Pages inactive'] * page_size
+
+ memory_facts['memfree_mb'] = memory_facts['memtotal_mb'] - (total_used // 1024 // 1024)
+
+ return memory_facts
+
+ def get_uptime_facts(self):
+ # On Darwin, the default format is annoying to parse.
+ # Use -b to get the raw value and decode it.
+ sysctl_cmd = self.module.get_bin_path('sysctl')
+ cmd = [sysctl_cmd, '-b', 'kern.boottime']
+
+ # We need to get raw bytes, not UTF-8.
+ rc, out, err = self.module.run_command(cmd, encoding=None)
+
+ # kern.boottime returns seconds and microseconds as two 64-bits
+ # fields, but we are only interested in the first field.
+ struct_format = '@L'
+ struct_size = struct.calcsize(struct_format)
+ if rc != 0 or len(out) < struct_size:
+ return {}
+
+ (kern_boottime, ) = struct.unpack(struct_format, out[:struct_size])
+
+ return {
+ 'uptime_seconds': int(time.time() - kern_boottime),
+ }
+
+
+class DarwinHardwareCollector(HardwareCollector):
+ _fact_class = DarwinHardware
+ _platform = 'Darwin'
diff --git a/lib/ansible/module_utils/facts/hardware/dragonfly.py b/lib/ansible/module_utils/facts/hardware/dragonfly.py
new file mode 100644
index 0000000..ea24151
--- /dev/null
+++ b/lib/ansible/module_utils/facts/hardware/dragonfly.py
@@ -0,0 +1,26 @@
+# 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
+
+from ansible.module_utils.facts.hardware.base import HardwareCollector
+from ansible.module_utils.facts.hardware.freebsd import FreeBSDHardware
+
+
+class DragonFlyHardwareCollector(HardwareCollector):
+ # Note: This uses the freebsd fact class, there is no dragonfly hardware fact class
+ _fact_class = FreeBSDHardware
+ _platform = 'DragonFly'
diff --git a/lib/ansible/module_utils/facts/hardware/freebsd.py b/lib/ansible/module_utils/facts/hardware/freebsd.py
new file mode 100644
index 0000000..cce2ab2
--- /dev/null
+++ b/lib/ansible/module_utils/facts/hardware/freebsd.py
@@ -0,0 +1,241 @@
+# 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 json
+import re
+import struct
+import time
+
+from ansible.module_utils.facts.hardware.base import Hardware, HardwareCollector
+from ansible.module_utils.facts.timeout import TimeoutError, timeout
+
+from ansible.module_utils.facts.utils import get_file_content, get_mount_size
+
+
+class FreeBSDHardware(Hardware):
+ """
+ FreeBSD-specific subclass of Hardware. Defines memory and CPU facts:
+ - memfree_mb
+ - memtotal_mb
+ - swapfree_mb
+ - swaptotal_mb
+ - processor (a list)
+ - processor_cores
+ - processor_count
+ - devices
+ - uptime_seconds
+ """
+ platform = 'FreeBSD'
+ DMESG_BOOT = '/var/run/dmesg.boot'
+
+ def populate(self, collected_facts=None):
+ hardware_facts = {}
+
+ cpu_facts = self.get_cpu_facts()
+ memory_facts = self.get_memory_facts()
+ uptime_facts = self.get_uptime_facts()
+ dmi_facts = self.get_dmi_facts()
+ device_facts = self.get_device_facts()
+
+ mount_facts = {}
+ try:
+ mount_facts = self.get_mount_facts()
+ except TimeoutError:
+ pass
+
+ hardware_facts.update(cpu_facts)
+ hardware_facts.update(memory_facts)
+ hardware_facts.update(uptime_facts)
+ hardware_facts.update(dmi_facts)
+ hardware_facts.update(device_facts)
+ hardware_facts.update(mount_facts)
+
+ return hardware_facts
+
+ def get_cpu_facts(self):
+ cpu_facts = {}
+ cpu_facts['processor'] = []
+ sysctl = self.module.get_bin_path('sysctl')
+ if sysctl:
+ rc, out, err = self.module.run_command("%s -n hw.ncpu" % sysctl, check_rc=False)
+ cpu_facts['processor_count'] = out.strip()
+
+ dmesg_boot = get_file_content(FreeBSDHardware.DMESG_BOOT)
+ if not dmesg_boot:
+ try:
+ rc, dmesg_boot, err = self.module.run_command(self.module.get_bin_path("dmesg"), check_rc=False)
+ except Exception:
+ dmesg_boot = ''
+
+ for line in dmesg_boot.splitlines():
+ if 'CPU:' in line:
+ cpu = re.sub(r'CPU:\s+', r"", line)
+ cpu_facts['processor'].append(cpu.strip())
+ if 'Logical CPUs per core' in line:
+ cpu_facts['processor_cores'] = line.split()[4]
+
+ return cpu_facts
+
+ def get_memory_facts(self):
+ memory_facts = {}
+
+ sysctl = self.module.get_bin_path('sysctl')
+ if sysctl:
+ rc, out, err = self.module.run_command("%s vm.stats" % sysctl, check_rc=False)
+ for line in out.splitlines():
+ data = line.split()
+ if 'vm.stats.vm.v_page_size' in line:
+ pagesize = int(data[1])
+ if 'vm.stats.vm.v_page_count' in line:
+ pagecount = int(data[1])
+ if 'vm.stats.vm.v_free_count' in line:
+ freecount = int(data[1])
+ memory_facts['memtotal_mb'] = pagesize * pagecount // 1024 // 1024
+ memory_facts['memfree_mb'] = pagesize * freecount // 1024 // 1024
+
+ swapinfo = self.module.get_bin_path('swapinfo')
+ if swapinfo:
+ # Get swapinfo. swapinfo output looks like:
+ # Device 1M-blocks Used Avail Capacity
+ # /dev/ada0p3 314368 0 314368 0%
+ #
+ rc, out, err = self.module.run_command("%s -k" % swapinfo)
+ lines = out.splitlines()
+ if len(lines[-1]) == 0:
+ lines.pop()
+ data = lines[-1].split()
+ if data[0] != 'Device':
+ memory_facts['swaptotal_mb'] = int(data[1]) // 1024
+ memory_facts['swapfree_mb'] = int(data[3]) // 1024
+
+ return memory_facts
+
+ def get_uptime_facts(self):
+ # On FreeBSD, the default format is annoying to parse.
+ # Use -b to get the raw value and decode it.
+ sysctl_cmd = self.module.get_bin_path('sysctl')
+ cmd = [sysctl_cmd, '-b', 'kern.boottime']
+
+ # We need to get raw bytes, not UTF-8.
+ rc, out, err = self.module.run_command(cmd, encoding=None)
+
+ # kern.boottime returns seconds and microseconds as two 64-bits
+ # fields, but we are only interested in the first field.
+ struct_format = '@L'
+ struct_size = struct.calcsize(struct_format)
+ if rc != 0 or len(out) < struct_size:
+ return {}
+
+ (kern_boottime, ) = struct.unpack(struct_format, out[:struct_size])
+
+ return {
+ 'uptime_seconds': int(time.time() - kern_boottime),
+ }
+
+ @timeout()
+ def get_mount_facts(self):
+ mount_facts = {}
+
+ mount_facts['mounts'] = []
+ fstab = get_file_content('/etc/fstab')
+ if fstab:
+ for line in fstab.splitlines():
+ if line.startswith('#') or line.strip() == '':
+ continue
+ fields = re.sub(r'\s+', ' ', line).split()
+ mount_statvfs_info = get_mount_size(fields[1])
+ mount_info = {'mount': fields[1],
+ 'device': fields[0],
+ 'fstype': fields[2],
+ 'options': fields[3]}
+ mount_info.update(mount_statvfs_info)
+ mount_facts['mounts'].append(mount_info)
+
+ return mount_facts
+
+ def get_device_facts(self):
+ device_facts = {}
+
+ sysdir = '/dev'
+ device_facts['devices'] = {}
+ drives = re.compile(r'(ada?\d+|da\d+|a?cd\d+)') # TODO: rc, disks, err = self.module.run_command("/sbin/sysctl kern.disks")
+ slices = re.compile(r'(ada?\d+s\d+\w*|da\d+s\d+\w*)')
+ if os.path.isdir(sysdir):
+ dirlist = sorted(os.listdir(sysdir))
+ for device in dirlist:
+ d = drives.match(device)
+ if d:
+ device_facts['devices'][d.group(1)] = []
+ s = slices.match(device)
+ if s:
+ device_facts['devices'][d.group(1)].append(s.group(1))
+
+ return device_facts
+
+ def get_dmi_facts(self):
+ ''' learn dmi facts from system
+
+ Use dmidecode executable if available'''
+
+ dmi_facts = {}
+
+ # Fall back to using dmidecode, if available
+ dmi_bin = self.module.get_bin_path('dmidecode')
+ DMI_DICT = {
+ 'bios_date': 'bios-release-date',
+ 'bios_vendor': 'bios-vendor',
+ 'bios_version': 'bios-version',
+ 'board_asset_tag': 'baseboard-asset-tag',
+ 'board_name': 'baseboard-product-name',
+ 'board_serial': 'baseboard-serial-number',
+ 'board_vendor': 'baseboard-manufacturer',
+ 'board_version': 'baseboard-version',
+ 'chassis_asset_tag': 'chassis-asset-tag',
+ 'chassis_serial': 'chassis-serial-number',
+ 'chassis_vendor': 'chassis-manufacturer',
+ 'chassis_version': 'chassis-version',
+ 'form_factor': 'chassis-type',
+ 'product_name': 'system-product-name',
+ 'product_serial': 'system-serial-number',
+ 'product_uuid': 'system-uuid',
+ 'product_version': 'system-version',
+ 'system_vendor': 'system-manufacturer',
+ }
+ for (k, v) in DMI_DICT.items():
+ if dmi_bin is not None:
+ (rc, out, err) = self.module.run_command('%s -s %s' % (dmi_bin, v))
+ if rc == 0:
+ # Strip out commented lines (specific dmidecode output)
+ # FIXME: why add the fact and then test if it is json?
+ dmi_facts[k] = ''.join([line for line in out.splitlines() if not line.startswith('#')])
+ try:
+ json.dumps(dmi_facts[k])
+ except UnicodeDecodeError:
+ dmi_facts[k] = 'NA'
+ else:
+ dmi_facts[k] = 'NA'
+ else:
+ dmi_facts[k] = 'NA'
+
+ return dmi_facts
+
+
+class FreeBSDHardwareCollector(HardwareCollector):
+ _fact_class = FreeBSDHardware
+ _platform = 'FreeBSD'
diff --git a/lib/ansible/module_utils/facts/hardware/hpux.py b/lib/ansible/module_utils/facts/hardware/hpux.py
new file mode 100644
index 0000000..ae72ed8
--- /dev/null
+++ b/lib/ansible/module_utils/facts/hardware/hpux.py
@@ -0,0 +1,165 @@
+# 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 re
+
+from ansible.module_utils.facts.hardware.base import Hardware, HardwareCollector
+
+
+class HPUXHardware(Hardware):
+ """
+ HP-UX-specific subclass of Hardware. Defines memory and CPU facts:
+ - memfree_mb
+ - memtotal_mb
+ - swapfree_mb
+ - swaptotal_mb
+ - processor
+ - processor_cores
+ - processor_count
+ - model
+ - firmware
+ """
+
+ platform = 'HP-UX'
+
+ def populate(self, collected_facts=None):
+ hardware_facts = {}
+
+ cpu_facts = self.get_cpu_facts(collected_facts=collected_facts)
+ memory_facts = self.get_memory_facts()
+ hw_facts = self.get_hw_facts()
+
+ hardware_facts.update(cpu_facts)
+ hardware_facts.update(memory_facts)
+ hardware_facts.update(hw_facts)
+
+ return hardware_facts
+
+ def get_cpu_facts(self, collected_facts=None):
+ cpu_facts = {}
+ collected_facts = collected_facts or {}
+
+ if collected_facts.get('ansible_architecture') in ['9000/800', '9000/785']:
+ rc, out, err = self.module.run_command("ioscan -FkCprocessor | wc -l", use_unsafe_shell=True)
+ cpu_facts['processor_count'] = int(out.strip())
+ # Working with machinfo mess
+ elif collected_facts.get('ansible_architecture') == 'ia64':
+ if collected_facts.get('ansible_distribution_version') == "B.11.23":
+ rc, out, err = self.module.run_command("/usr/contrib/bin/machinfo | grep 'Number of CPUs'", use_unsafe_shell=True)
+ if out:
+ cpu_facts['processor_count'] = int(out.strip().split('=')[1])
+ rc, out, err = self.module.run_command("/usr/contrib/bin/machinfo | grep 'processor family'", use_unsafe_shell=True)
+ if out:
+ cpu_facts['processor'] = re.search('.*(Intel.*)', out).groups()[0].strip()
+ rc, out, err = self.module.run_command("ioscan -FkCprocessor | wc -l", use_unsafe_shell=True)
+ cpu_facts['processor_cores'] = int(out.strip())
+ if collected_facts.get('ansible_distribution_version') == "B.11.31":
+ # if machinfo return cores strings release B.11.31 > 1204
+ rc, out, err = self.module.run_command("/usr/contrib/bin/machinfo | grep core | wc -l", use_unsafe_shell=True)
+ if out.strip() == '0':
+ rc, out, err = self.module.run_command("/usr/contrib/bin/machinfo | grep Intel", use_unsafe_shell=True)
+ cpu_facts['processor_count'] = int(out.strip().split(" ")[0])
+ # If hyperthreading is active divide cores by 2
+ rc, out, err = self.module.run_command("/usr/sbin/psrset | grep LCPU", use_unsafe_shell=True)
+ data = re.sub(' +', ' ', out).strip().split(' ')
+ if len(data) == 1:
+ hyperthreading = 'OFF'
+ else:
+ hyperthreading = data[1]
+ rc, out, err = self.module.run_command("/usr/contrib/bin/machinfo | grep logical", use_unsafe_shell=True)
+ data = out.strip().split(" ")
+ if hyperthreading == 'ON':
+ cpu_facts['processor_cores'] = int(data[0]) / 2
+ else:
+ if len(data) == 1:
+ cpu_facts['processor_cores'] = cpu_facts['processor_count']
+ else:
+ cpu_facts['processor_cores'] = int(data[0])
+ rc, out, err = self.module.run_command("/usr/contrib/bin/machinfo | grep Intel |cut -d' ' -f4-", use_unsafe_shell=True)
+ cpu_facts['processor'] = out.strip()
+ else:
+ rc, out, err = self.module.run_command("/usr/contrib/bin/machinfo | egrep 'socket[s]?$' | tail -1", use_unsafe_shell=True)
+ cpu_facts['processor_count'] = int(out.strip().split(" ")[0])
+ rc, out, err = self.module.run_command("/usr/contrib/bin/machinfo | grep -e '[0-9] core' | tail -1", use_unsafe_shell=True)
+ cpu_facts['processor_cores'] = int(out.strip().split(" ")[0])
+ rc, out, err = self.module.run_command("/usr/contrib/bin/machinfo | grep Intel", use_unsafe_shell=True)
+ cpu_facts['processor'] = out.strip()
+
+ return cpu_facts
+
+ def get_memory_facts(self, collected_facts=None):
+ memory_facts = {}
+ collected_facts = collected_facts or {}
+
+ pagesize = 4096
+ rc, out, err = self.module.run_command("/usr/bin/vmstat | tail -1", use_unsafe_shell=True)
+ data = int(re.sub(' +', ' ', out).split(' ')[5].strip())
+ memory_facts['memfree_mb'] = pagesize * data // 1024 // 1024
+ if collected_facts.get('ansible_architecture') in ['9000/800', '9000/785']:
+ try:
+ rc, out, err = self.module.run_command("grep Physical /var/adm/syslog/syslog.log")
+ data = re.search('.*Physical: ([0-9]*) Kbytes.*', out).groups()[0].strip()
+ memory_facts['memtotal_mb'] = int(data) // 1024
+ except AttributeError:
+ # For systems where memory details aren't sent to syslog or the log has rotated, use parsed
+ # adb output. Unfortunately /dev/kmem doesn't have world-read, so this only works as root.
+ if os.access("/dev/kmem", os.R_OK):
+ rc, out, err = self.module.run_command("echo 'phys_mem_pages/D' | adb -k /stand/vmunix /dev/kmem | tail -1 | awk '{print $2}'",
+ use_unsafe_shell=True)
+ if not err:
+ data = out
+ memory_facts['memtotal_mb'] = int(data) / 256
+ else:
+ rc, out, err = self.module.run_command("/usr/contrib/bin/machinfo | grep Memory", use_unsafe_shell=True)
+ data = re.search(r'Memory[\ :=]*([0-9]*).*MB.*', out).groups()[0].strip()
+ memory_facts['memtotal_mb'] = int(data)
+ rc, out, err = self.module.run_command("/usr/sbin/swapinfo -m -d -f -q")
+ memory_facts['swaptotal_mb'] = int(out.strip())
+ rc, out, err = self.module.run_command("/usr/sbin/swapinfo -m -d -f | egrep '^dev|^fs'", use_unsafe_shell=True)
+ swap = 0
+ for line in out.strip().splitlines():
+ swap += int(re.sub(' +', ' ', line).split(' ')[3].strip())
+ memory_facts['swapfree_mb'] = swap
+
+ return memory_facts
+
+ def get_hw_facts(self, collected_facts=None):
+ hw_facts = {}
+ collected_facts = collected_facts or {}
+
+ rc, out, err = self.module.run_command("model")
+ hw_facts['model'] = out.strip()
+ if collected_facts.get('ansible_architecture') == 'ia64':
+ separator = ':'
+ if collected_facts.get('ansible_distribution_version') == "B.11.23":
+ separator = '='
+ rc, out, err = self.module.run_command("/usr/contrib/bin/machinfo |grep -i 'Firmware revision' | grep -v BMC", use_unsafe_shell=True)
+ hw_facts['firmware_version'] = out.split(separator)[1].strip()
+ rc, out, err = self.module.run_command("/usr/contrib/bin/machinfo |grep -i 'Machine serial number' ", use_unsafe_shell=True)
+ if rc == 0 and out:
+ hw_facts['product_serial'] = out.split(separator)[1].strip()
+
+ return hw_facts
+
+
+class HPUXHardwareCollector(HardwareCollector):
+ _fact_class = HPUXHardware
+ _platform = 'HP-UX'
+
+ required_facts = set(['platform', 'distribution'])
diff --git a/lib/ansible/module_utils/facts/hardware/hurd.py b/lib/ansible/module_utils/facts/hardware/hurd.py
new file mode 100644
index 0000000..306e13c
--- /dev/null
+++ b/lib/ansible/module_utils/facts/hardware/hurd.py
@@ -0,0 +1,53 @@
+# 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
+
+from ansible.module_utils.facts.timeout import TimeoutError
+from ansible.module_utils.facts.hardware.base import HardwareCollector
+from ansible.module_utils.facts.hardware.linux import LinuxHardware
+
+
+class HurdHardware(LinuxHardware):
+ """
+ GNU Hurd specific subclass of Hardware. Define memory and mount facts
+ based on procfs compatibility translator mimicking the interface of
+ the Linux kernel.
+ """
+
+ platform = 'GNU'
+
+ def populate(self, collected_facts=None):
+ hardware_facts = {}
+ uptime_facts = self.get_uptime_facts()
+ memory_facts = self.get_memory_facts()
+
+ mount_facts = {}
+ try:
+ mount_facts = self.get_mount_facts()
+ except TimeoutError:
+ pass
+
+ hardware_facts.update(uptime_facts)
+ hardware_facts.update(memory_facts)
+ hardware_facts.update(mount_facts)
+
+ return hardware_facts
+
+
+class HurdHardwareCollector(HardwareCollector):
+ _fact_class = HurdHardware
+ _platform = 'GNU'
diff --git a/lib/ansible/module_utils/facts/hardware/linux.py b/lib/ansible/module_utils/facts/hardware/linux.py
new file mode 100644
index 0000000..c0ca33d
--- /dev/null
+++ b/lib/ansible/module_utils/facts/hardware/linux.py
@@ -0,0 +1,869 @@
+# 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 collections
+import errno
+import glob
+import json
+import os
+import re
+import sys
+import time
+
+from multiprocessing import cpu_count
+from multiprocessing.pool import ThreadPool
+
+from ansible.module_utils._text import to_text
+from ansible.module_utils.common.locale import get_best_parsable_locale
+from ansible.module_utils.common.process import get_bin_path
+from ansible.module_utils.common.text.formatters import bytes_to_human
+from ansible.module_utils.facts.hardware.base import Hardware, HardwareCollector
+from ansible.module_utils.facts.utils import get_file_content, get_file_lines, get_mount_size
+from ansible.module_utils.six import iteritems
+
+# import this as a module to ensure we get the same module instance
+from ansible.module_utils.facts import timeout
+
+
+def get_partition_uuid(partname):
+ try:
+ uuids = os.listdir("/dev/disk/by-uuid")
+ except OSError:
+ return
+
+ for uuid in uuids:
+ dev = os.path.realpath("/dev/disk/by-uuid/" + uuid)
+ if dev == ("/dev/" + partname):
+ return uuid
+
+ return None
+
+
+class LinuxHardware(Hardware):
+ """
+ Linux-specific subclass of Hardware. Defines memory and CPU facts:
+ - memfree_mb
+ - memtotal_mb
+ - swapfree_mb
+ - swaptotal_mb
+ - processor (a list)
+ - processor_cores
+ - processor_count
+
+ In addition, it also defines number of DMI facts and device facts.
+ """
+
+ platform = 'Linux'
+
+ # Originally only had these four as toplevelfacts
+ ORIGINAL_MEMORY_FACTS = frozenset(('MemTotal', 'SwapTotal', 'MemFree', 'SwapFree'))
+ # Now we have all of these in a dict structure
+ MEMORY_FACTS = ORIGINAL_MEMORY_FACTS.union(('Buffers', 'Cached', 'SwapCached'))
+
+ # regex used against findmnt output to detect bind mounts
+ BIND_MOUNT_RE = re.compile(r'.*\]')
+
+ # regex used against mtab content to find entries that are bind mounts
+ MTAB_BIND_MOUNT_RE = re.compile(r'.*bind.*"')
+
+ # regex used for replacing octal escape sequences
+ OCTAL_ESCAPE_RE = re.compile(r'\\[0-9]{3}')
+
+ def populate(self, collected_facts=None):
+ hardware_facts = {}
+ locale = get_best_parsable_locale(self.module)
+ self.module.run_command_environ_update = {'LANG': locale, 'LC_ALL': locale, 'LC_NUMERIC': locale}
+
+ cpu_facts = self.get_cpu_facts(collected_facts=collected_facts)
+ memory_facts = self.get_memory_facts()
+ dmi_facts = self.get_dmi_facts()
+ device_facts = self.get_device_facts()
+ uptime_facts = self.get_uptime_facts()
+ lvm_facts = self.get_lvm_facts()
+
+ mount_facts = {}
+ try:
+ mount_facts = self.get_mount_facts()
+ except timeout.TimeoutError:
+ self.module.warn("No mount facts were gathered due to timeout.")
+
+ hardware_facts.update(cpu_facts)
+ hardware_facts.update(memory_facts)
+ hardware_facts.update(dmi_facts)
+ hardware_facts.update(device_facts)
+ hardware_facts.update(uptime_facts)
+ hardware_facts.update(lvm_facts)
+ hardware_facts.update(mount_facts)
+
+ return hardware_facts
+
+ def get_memory_facts(self):
+ memory_facts = {}
+ if not os.access("/proc/meminfo", os.R_OK):
+ return memory_facts
+
+ memstats = {}
+ for line in get_file_lines("/proc/meminfo"):
+ data = line.split(":", 1)
+ key = data[0]
+ if key in self.ORIGINAL_MEMORY_FACTS:
+ val = data[1].strip().split(' ')[0]
+ memory_facts["%s_mb" % key.lower()] = int(val) // 1024
+
+ if key in self.MEMORY_FACTS:
+ val = data[1].strip().split(' ')[0]
+ memstats[key.lower()] = int(val) // 1024
+
+ if None not in (memstats.get('memtotal'), memstats.get('memfree')):
+ memstats['real:used'] = memstats['memtotal'] - memstats['memfree']
+ if None not in (memstats.get('cached'), memstats.get('memfree'), memstats.get('buffers')):
+ memstats['nocache:free'] = memstats['cached'] + memstats['memfree'] + memstats['buffers']
+ if None not in (memstats.get('memtotal'), memstats.get('nocache:free')):
+ memstats['nocache:used'] = memstats['memtotal'] - memstats['nocache:free']
+ if None not in (memstats.get('swaptotal'), memstats.get('swapfree')):
+ memstats['swap:used'] = memstats['swaptotal'] - memstats['swapfree']
+
+ memory_facts['memory_mb'] = {
+ 'real': {
+ 'total': memstats.get('memtotal'),
+ 'used': memstats.get('real:used'),
+ 'free': memstats.get('memfree'),
+ },
+ 'nocache': {
+ 'free': memstats.get('nocache:free'),
+ 'used': memstats.get('nocache:used'),
+ },
+ 'swap': {
+ 'total': memstats.get('swaptotal'),
+ 'free': memstats.get('swapfree'),
+ 'used': memstats.get('swap:used'),
+ 'cached': memstats.get('swapcached'),
+ },
+ }
+
+ return memory_facts
+
+ def get_cpu_facts(self, collected_facts=None):
+ cpu_facts = {}
+ collected_facts = collected_facts or {}
+
+ i = 0
+ vendor_id_occurrence = 0
+ model_name_occurrence = 0
+ processor_occurrence = 0
+ physid = 0
+ coreid = 0
+ sockets = {}
+ cores = {}
+
+ xen = False
+ xen_paravirt = False
+ try:
+ if os.path.exists('/proc/xen'):
+ xen = True
+ else:
+ for line in get_file_lines('/sys/hypervisor/type'):
+ if line.strip() == 'xen':
+ xen = True
+ # Only interested in the first line
+ break
+ except IOError:
+ pass
+
+ if not os.access("/proc/cpuinfo", os.R_OK):
+ return cpu_facts
+
+ cpu_facts['processor'] = []
+ for line in get_file_lines('/proc/cpuinfo'):
+ data = line.split(":", 1)
+ key = data[0].strip()
+
+ try:
+ val = data[1].strip()
+ except IndexError:
+ val = ""
+
+ if xen:
+ if key == 'flags':
+ # Check for vme cpu flag, Xen paravirt does not expose this.
+ # Need to detect Xen paravirt because it exposes cpuinfo
+ # differently than Xen HVM or KVM and causes reporting of
+ # only a single cpu core.
+ if 'vme' not in val:
+ xen_paravirt = True
+
+ # model name is for Intel arch, Processor (mind the uppercase P)
+ # works for some ARM devices, like the Sheevaplug.
+ # 'ncpus active' is SPARC attribute
+ if key in ['model name', 'Processor', 'vendor_id', 'cpu', 'Vendor', 'processor']:
+ if 'processor' not in cpu_facts:
+ cpu_facts['processor'] = []
+ cpu_facts['processor'].append(val)
+ if key == 'vendor_id':
+ vendor_id_occurrence += 1
+ if key == 'model name':
+ model_name_occurrence += 1
+ if key == 'processor':
+ processor_occurrence += 1
+ i += 1
+ elif key == 'physical id':
+ physid = val
+ if physid not in sockets:
+ sockets[physid] = 1
+ elif key == 'core id':
+ coreid = val
+ if coreid not in sockets:
+ cores[coreid] = 1
+ elif key == 'cpu cores':
+ sockets[physid] = int(val)
+ elif key == 'siblings':
+ cores[coreid] = int(val)
+ elif key == '# processors':
+ cpu_facts['processor_cores'] = int(val)
+ elif key == 'ncpus active':
+ i = int(val)
+
+ # Skip for platforms without vendor_id/model_name in cpuinfo (e.g ppc64le)
+ if vendor_id_occurrence > 0:
+ if vendor_id_occurrence == model_name_occurrence:
+ i = vendor_id_occurrence
+
+ # The fields for ARM CPUs do not always include 'vendor_id' or 'model name',
+ # and sometimes includes both 'processor' and 'Processor'.
+ # The fields for Power CPUs include 'processor' and 'cpu'.
+ # Always use 'processor' count for ARM and Power systems
+ if collected_facts.get('ansible_architecture', '').startswith(('armv', 'aarch', 'ppc')):
+ i = processor_occurrence
+
+ # FIXME
+ if collected_facts.get('ansible_architecture') != 's390x':
+ if xen_paravirt:
+ cpu_facts['processor_count'] = i
+ cpu_facts['processor_cores'] = i
+ cpu_facts['processor_threads_per_core'] = 1
+ cpu_facts['processor_vcpus'] = i
+ else:
+ if sockets:
+ cpu_facts['processor_count'] = len(sockets)
+ else:
+ cpu_facts['processor_count'] = i
+
+ socket_values = list(sockets.values())
+ if socket_values and socket_values[0]:
+ cpu_facts['processor_cores'] = socket_values[0]
+ else:
+ cpu_facts['processor_cores'] = 1
+
+ core_values = list(cores.values())
+ if core_values:
+ cpu_facts['processor_threads_per_core'] = core_values[0] // cpu_facts['processor_cores']
+ else:
+ cpu_facts['processor_threads_per_core'] = 1 // cpu_facts['processor_cores']
+
+ cpu_facts['processor_vcpus'] = (cpu_facts['processor_threads_per_core'] *
+ cpu_facts['processor_count'] * cpu_facts['processor_cores'])
+
+ # if the number of processors available to the module's
+ # thread cannot be determined, the processor count
+ # reported by /proc will be the default:
+ cpu_facts['processor_nproc'] = processor_occurrence
+
+ try:
+ cpu_facts['processor_nproc'] = len(
+ os.sched_getaffinity(0)
+ )
+ except AttributeError:
+ # In Python < 3.3, os.sched_getaffinity() is not available
+ try:
+ cmd = get_bin_path('nproc')
+ except ValueError:
+ pass
+ else:
+ rc, out, _err = self.module.run_command(cmd)
+ if rc == 0:
+ cpu_facts['processor_nproc'] = int(out)
+
+ return cpu_facts
+
+ def get_dmi_facts(self):
+ ''' learn dmi facts from system
+
+ Try /sys first for dmi related facts.
+ If that is not available, fall back to dmidecode executable '''
+
+ dmi_facts = {}
+
+ if os.path.exists('/sys/devices/virtual/dmi/id/product_name'):
+ # Use kernel DMI info, if available
+
+ # DMI SPEC -- https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.2.0.pdf
+ FORM_FACTOR = ["Unknown", "Other", "Unknown", "Desktop",
+ "Low Profile Desktop", "Pizza Box", "Mini Tower", "Tower",
+ "Portable", "Laptop", "Notebook", "Hand Held", "Docking Station",
+ "All In One", "Sub Notebook", "Space-saving", "Lunch Box",
+ "Main Server Chassis", "Expansion Chassis", "Sub Chassis",
+ "Bus Expansion Chassis", "Peripheral Chassis", "RAID Chassis",
+ "Rack Mount Chassis", "Sealed-case PC", "Multi-system",
+ "CompactPCI", "AdvancedTCA", "Blade", "Blade Enclosure",
+ "Tablet", "Convertible", "Detachable", "IoT Gateway",
+ "Embedded PC", "Mini PC", "Stick PC"]
+
+ DMI_DICT = {
+ 'bios_date': '/sys/devices/virtual/dmi/id/bios_date',
+ 'bios_vendor': '/sys/devices/virtual/dmi/id/bios_vendor',
+ 'bios_version': '/sys/devices/virtual/dmi/id/bios_version',
+ 'board_asset_tag': '/sys/devices/virtual/dmi/id/board_asset_tag',
+ 'board_name': '/sys/devices/virtual/dmi/id/board_name',
+ 'board_serial': '/sys/devices/virtual/dmi/id/board_serial',
+ 'board_vendor': '/sys/devices/virtual/dmi/id/board_vendor',
+ 'board_version': '/sys/devices/virtual/dmi/id/board_version',
+ 'chassis_asset_tag': '/sys/devices/virtual/dmi/id/chassis_asset_tag',
+ 'chassis_serial': '/sys/devices/virtual/dmi/id/chassis_serial',
+ 'chassis_vendor': '/sys/devices/virtual/dmi/id/chassis_vendor',
+ 'chassis_version': '/sys/devices/virtual/dmi/id/chassis_version',
+ 'form_factor': '/sys/devices/virtual/dmi/id/chassis_type',
+ 'product_name': '/sys/devices/virtual/dmi/id/product_name',
+ 'product_serial': '/sys/devices/virtual/dmi/id/product_serial',
+ 'product_uuid': '/sys/devices/virtual/dmi/id/product_uuid',
+ 'product_version': '/sys/devices/virtual/dmi/id/product_version',
+ 'system_vendor': '/sys/devices/virtual/dmi/id/sys_vendor',
+ }
+
+ for (key, path) in DMI_DICT.items():
+ data = get_file_content(path)
+ if data is not None:
+ if key == 'form_factor':
+ try:
+ dmi_facts['form_factor'] = FORM_FACTOR[int(data)]
+ except IndexError:
+ dmi_facts['form_factor'] = 'unknown (%s)' % data
+ else:
+ dmi_facts[key] = data
+ else:
+ dmi_facts[key] = 'NA'
+
+ else:
+ # Fall back to using dmidecode, if available
+ dmi_bin = self.module.get_bin_path('dmidecode')
+ DMI_DICT = {
+ 'bios_date': 'bios-release-date',
+ 'bios_vendor': 'bios-vendor',
+ 'bios_version': 'bios-version',
+ 'board_asset_tag': 'baseboard-asset-tag',
+ 'board_name': 'baseboard-product-name',
+ 'board_serial': 'baseboard-serial-number',
+ 'board_vendor': 'baseboard-manufacturer',
+ 'board_version': 'baseboard-version',
+ 'chassis_asset_tag': 'chassis-asset-tag',
+ 'chassis_serial': 'chassis-serial-number',
+ 'chassis_vendor': 'chassis-manufacturer',
+ 'chassis_version': 'chassis-version',
+ 'form_factor': 'chassis-type',
+ 'product_name': 'system-product-name',
+ 'product_serial': 'system-serial-number',
+ 'product_uuid': 'system-uuid',
+ 'product_version': 'system-version',
+ 'system_vendor': 'system-manufacturer',
+ }
+ for (k, v) in DMI_DICT.items():
+ if dmi_bin is not None:
+ (rc, out, err) = self.module.run_command('%s -s %s' % (dmi_bin, v))
+ if rc == 0:
+ # Strip out commented lines (specific dmidecode output)
+ thisvalue = ''.join([line for line in out.splitlines() if not line.startswith('#')])
+ try:
+ json.dumps(thisvalue)
+ except UnicodeDecodeError:
+ thisvalue = "NA"
+
+ dmi_facts[k] = thisvalue
+ else:
+ dmi_facts[k] = 'NA'
+ else:
+ dmi_facts[k] = 'NA'
+
+ return dmi_facts
+
+ def _run_lsblk(self, lsblk_path):
+ # call lsblk and collect all uuids
+ # --exclude 2 makes lsblk ignore floppy disks, which are slower to answer than typical timeouts
+ # this uses the linux major device number
+ # for details see https://www.kernel.org/doc/Documentation/devices.txt
+ args = ['--list', '--noheadings', '--paths', '--output', 'NAME,UUID', '--exclude', '2']
+ cmd = [lsblk_path] + args
+ rc, out, err = self.module.run_command(cmd)
+ return rc, out, err
+
+ def _lsblk_uuid(self):
+ uuids = {}
+ lsblk_path = self.module.get_bin_path("lsblk")
+ if not lsblk_path:
+ return uuids
+
+ rc, out, err = self._run_lsblk(lsblk_path)
+ if rc != 0:
+ return uuids
+
+ # each line will be in format:
+ # <devicename><some whitespace><uuid>
+ # /dev/sda1 32caaec3-ef40-4691-a3b6-438c3f9bc1c0
+ for lsblk_line in out.splitlines():
+ if not lsblk_line:
+ continue
+
+ line = lsblk_line.strip()
+ fields = line.rsplit(None, 1)
+
+ if len(fields) < 2:
+ continue
+
+ device_name, uuid = fields[0].strip(), fields[1].strip()
+ if device_name in uuids:
+ continue
+ uuids[device_name] = uuid
+
+ return uuids
+
+ def _udevadm_uuid(self, device):
+ # fallback for versions of lsblk <= 2.23 that don't have --paths, see _run_lsblk() above
+ uuid = 'N/A'
+
+ udevadm_path = self.module.get_bin_path('udevadm')
+ if not udevadm_path:
+ return uuid
+
+ cmd = [udevadm_path, 'info', '--query', 'property', '--name', device]
+ rc, out, err = self.module.run_command(cmd)
+ if rc != 0:
+ return uuid
+
+ # a snippet of the output of the udevadm command below will be:
+ # ...
+ # ID_FS_TYPE=ext4
+ # ID_FS_USAGE=filesystem
+ # ID_FS_UUID=57b1a3e7-9019-4747-9809-7ec52bba9179
+ # ...
+ m = re.search('ID_FS_UUID=(.*)\n', out)
+ if m:
+ uuid = m.group(1)
+
+ return uuid
+
+ def _run_findmnt(self, findmnt_path):
+ args = ['--list', '--noheadings', '--notruncate']
+ cmd = [findmnt_path] + args
+ rc, out, err = self.module.run_command(cmd, errors='surrogate_then_replace')
+ return rc, out, err
+
+ def _find_bind_mounts(self):
+ bind_mounts = set()
+ findmnt_path = self.module.get_bin_path("findmnt")
+ if not findmnt_path:
+ return bind_mounts
+
+ rc, out, err = self._run_findmnt(findmnt_path)
+ if rc != 0:
+ return bind_mounts
+
+ # find bind mounts, in case /etc/mtab is a symlink to /proc/mounts
+ for line in out.splitlines():
+ fields = line.split()
+ # fields[0] is the TARGET, fields[1] is the SOURCE
+ if len(fields) < 2:
+ continue
+
+ # bind mounts will have a [/directory_name] in the SOURCE column
+ if self.BIND_MOUNT_RE.match(fields[1]):
+ bind_mounts.add(fields[0])
+
+ return bind_mounts
+
+ def _mtab_entries(self):
+ mtab_file = '/etc/mtab'
+ if not os.path.exists(mtab_file):
+ mtab_file = '/proc/mounts'
+
+ mtab = get_file_content(mtab_file, '')
+ mtab_entries = []
+ for line in mtab.splitlines():
+ fields = line.split()
+ if len(fields) < 4:
+ continue
+ mtab_entries.append(fields)
+ return mtab_entries
+
+ @staticmethod
+ def _replace_octal_escapes_helper(match):
+ # Convert to integer using base8 and then convert to character
+ return chr(int(match.group()[1:], 8))
+
+ def _replace_octal_escapes(self, value):
+ return self.OCTAL_ESCAPE_RE.sub(self._replace_octal_escapes_helper, value)
+
+ def get_mount_info(self, mount, device, uuids):
+
+ mount_size = get_mount_size(mount)
+
+ # _udevadm_uuid is a fallback for versions of lsblk <= 2.23 that don't have --paths
+ # see _run_lsblk() above
+ # https://github.com/ansible/ansible/issues/36077
+ uuid = uuids.get(device, self._udevadm_uuid(device))
+
+ return mount_size, uuid
+
+ def get_mount_facts(self):
+
+ mounts = []
+
+ # gather system lists
+ bind_mounts = self._find_bind_mounts()
+ uuids = self._lsblk_uuid()
+ mtab_entries = self._mtab_entries()
+
+ # start threads to query each mount
+ results = {}
+ pool = ThreadPool(processes=min(len(mtab_entries), cpu_count()))
+ maxtime = globals().get('GATHER_TIMEOUT') or timeout.DEFAULT_GATHER_TIMEOUT
+ for fields in mtab_entries:
+ # Transform octal escape sequences
+ fields = [self._replace_octal_escapes(field) for field in fields]
+
+ device, mount, fstype, options = fields[0], fields[1], fields[2], fields[3]
+
+ if not device.startswith(('/', '\\')) and ':/' not in device or fstype == 'none':
+ continue
+
+ mount_info = {'mount': mount,
+ 'device': device,
+ 'fstype': fstype,
+ 'options': options}
+
+ if mount in bind_mounts:
+ # only add if not already there, we might have a plain /etc/mtab
+ if not self.MTAB_BIND_MOUNT_RE.match(options):
+ mount_info['options'] += ",bind"
+
+ results[mount] = {'info': mount_info,
+ 'extra': pool.apply_async(self.get_mount_info, (mount, device, uuids)),
+ 'timelimit': time.time() + maxtime}
+
+ pool.close() # done with new workers, start gc
+
+ # wait for workers and get results
+ while results:
+ for mount in list(results):
+ done = False
+ res = results[mount]['extra']
+ try:
+ if res.ready():
+ done = True
+ if res.successful():
+ mount_size, uuid = res.get()
+ if mount_size:
+ results[mount]['info'].update(mount_size)
+ results[mount]['info']['uuid'] = uuid or 'N/A'
+ else:
+ # failed, try to find out why, if 'res.successful' we know there are no exceptions
+ results[mount]['info']['note'] = 'Could not get extra information: %s.' % (to_text(res.get()))
+
+ elif time.time() > results[mount]['timelimit']:
+ done = True
+ self.module.warn("Timeout exceeded when getting mount info for %s" % mount)
+ results[mount]['info']['note'] = 'Could not get extra information due to timeout'
+ except Exception as e:
+ import traceback
+ done = True
+ results[mount]['info'] = 'N/A'
+ self.module.warn("Error prevented getting extra info for mount %s: [%s] %s." % (mount, type(e), to_text(e)))
+ self.module.debug(traceback.format_exc())
+
+ if done:
+ # move results outside and make loop only handle pending
+ mounts.append(results[mount]['info'])
+ del results[mount]
+
+ # avoid cpu churn, sleep between retrying for loop with remaining mounts
+ time.sleep(0.1)
+
+ return {'mounts': mounts}
+
+ def get_device_links(self, link_dir):
+ if not os.path.exists(link_dir):
+ return {}
+ try:
+ retval = collections.defaultdict(set)
+ for entry in os.listdir(link_dir):
+ try:
+ target = os.path.basename(os.readlink(os.path.join(link_dir, entry)))
+ retval[target].add(entry)
+ except OSError:
+ continue
+ return dict((k, list(sorted(v))) for (k, v) in iteritems(retval))
+ except OSError:
+ return {}
+
+ def get_all_device_owners(self):
+ try:
+ retval = collections.defaultdict(set)
+ for path in glob.glob('/sys/block/*/slaves/*'):
+ elements = path.split('/')
+ device = elements[3]
+ target = elements[5]
+ retval[target].add(device)
+ return dict((k, list(sorted(v))) for (k, v) in iteritems(retval))
+ except OSError:
+ return {}
+
+ def get_all_device_links(self):
+ return {
+ 'ids': self.get_device_links('/dev/disk/by-id'),
+ 'uuids': self.get_device_links('/dev/disk/by-uuid'),
+ 'labels': self.get_device_links('/dev/disk/by-label'),
+ 'masters': self.get_all_device_owners(),
+ }
+
+ def get_holders(self, block_dev_dict, sysdir):
+ block_dev_dict['holders'] = []
+ if os.path.isdir(sysdir + "/holders"):
+ for folder in os.listdir(sysdir + "/holders"):
+ if not folder.startswith("dm-"):
+ continue
+ name = get_file_content(sysdir + "/holders/" + folder + "/dm/name")
+ if name:
+ block_dev_dict['holders'].append(name)
+ else:
+ block_dev_dict['holders'].append(folder)
+
+ def _get_sg_inq_serial(self, sg_inq, block):
+ device = "/dev/%s" % (block)
+ rc, drivedata, err = self.module.run_command([sg_inq, device])
+ if rc == 0:
+ serial = re.search(r"(?:Unit serial|Serial) number:\s+(\w+)", drivedata)
+ if serial:
+ return serial.group(1)
+
+ def get_device_facts(self):
+ device_facts = {}
+
+ device_facts['devices'] = {}
+ lspci = self.module.get_bin_path('lspci')
+ if lspci:
+ rc, pcidata, err = self.module.run_command([lspci, '-D'], errors='surrogate_then_replace')
+ else:
+ pcidata = None
+
+ try:
+ block_devs = os.listdir("/sys/block")
+ except OSError:
+ return device_facts
+
+ devs_wwn = {}
+ try:
+ devs_by_id = os.listdir("/dev/disk/by-id")
+ except OSError:
+ pass
+ else:
+ for link_name in devs_by_id:
+ if link_name.startswith("wwn-"):
+ try:
+ wwn_link = os.readlink(os.path.join("/dev/disk/by-id", link_name))
+ except OSError:
+ continue
+ devs_wwn[os.path.basename(wwn_link)] = link_name[4:]
+
+ links = self.get_all_device_links()
+ device_facts['device_links'] = links
+
+ for block in block_devs:
+ virtual = 1
+ sysfs_no_links = 0
+ try:
+ path = os.readlink(os.path.join("/sys/block/", block))
+ except OSError:
+ e = sys.exc_info()[1]
+ if e.errno == errno.EINVAL:
+ path = block
+ sysfs_no_links = 1
+ else:
+ continue
+ sysdir = os.path.join("/sys/block", path)
+ if sysfs_no_links == 1:
+ for folder in os.listdir(sysdir):
+ if "device" in folder:
+ virtual = 0
+ break
+ d = {}
+ d['virtual'] = virtual
+ d['links'] = {}
+ for (link_type, link_values) in iteritems(links):
+ d['links'][link_type] = link_values.get(block, [])
+ diskname = os.path.basename(sysdir)
+ for key in ['vendor', 'model', 'sas_address', 'sas_device_handle']:
+ d[key] = get_file_content(sysdir + "/device/" + key)
+
+ sg_inq = self.module.get_bin_path('sg_inq')
+
+ # we can get NVMe device's serial number from /sys/block/<name>/device/serial
+ serial_path = "/sys/block/%s/device/serial" % (block)
+
+ if sg_inq:
+ serial = self._get_sg_inq_serial(sg_inq, block)
+ if serial:
+ d['serial'] = serial
+ else:
+ serial = get_file_content(serial_path)
+ if serial:
+ d['serial'] = serial
+
+ for key, test in [('removable', '/removable'),
+ ('support_discard', '/queue/discard_granularity'),
+ ]:
+ d[key] = get_file_content(sysdir + test)
+
+ if diskname in devs_wwn:
+ d['wwn'] = devs_wwn[diskname]
+
+ d['partitions'] = {}
+ for folder in os.listdir(sysdir):
+ m = re.search("(" + diskname + r"[p]?\d+)", folder)
+ if m:
+ part = {}
+ partname = m.group(1)
+ part_sysdir = sysdir + "/" + partname
+
+ part['links'] = {}
+ for (link_type, link_values) in iteritems(links):
+ part['links'][link_type] = link_values.get(partname, [])
+
+ part['start'] = get_file_content(part_sysdir + "/start", 0)
+ part['sectors'] = get_file_content(part_sysdir + "/size", 0)
+
+ part['sectorsize'] = get_file_content(part_sysdir + "/queue/logical_block_size")
+ if not part['sectorsize']:
+ part['sectorsize'] = get_file_content(part_sysdir + "/queue/hw_sector_size", 512)
+ part['size'] = bytes_to_human((float(part['sectors']) * 512.0))
+ part['uuid'] = get_partition_uuid(partname)
+ self.get_holders(part, part_sysdir)
+
+ d['partitions'][partname] = part
+
+ d['rotational'] = get_file_content(sysdir + "/queue/rotational")
+ d['scheduler_mode'] = ""
+ scheduler = get_file_content(sysdir + "/queue/scheduler")
+ if scheduler is not None:
+ m = re.match(r".*?(\[(.*)\])", scheduler)
+ if m:
+ d['scheduler_mode'] = m.group(2)
+
+ d['sectors'] = get_file_content(sysdir + "/size")
+ if not d['sectors']:
+ d['sectors'] = 0
+ d['sectorsize'] = get_file_content(sysdir + "/queue/logical_block_size")
+ if not d['sectorsize']:
+ d['sectorsize'] = get_file_content(sysdir + "/queue/hw_sector_size", 512)
+ d['size'] = bytes_to_human(float(d['sectors']) * 512.0)
+
+ d['host'] = ""
+
+ # domains are numbered (0 to ffff), bus (0 to ff), slot (0 to 1f), and function (0 to 7).
+ m = re.match(r".+/([a-f0-9]{4}:[a-f0-9]{2}:[0|1][a-f0-9]\.[0-7])/", sysdir)
+ if m and pcidata:
+ pciid = m.group(1)
+ did = re.escape(pciid)
+ m = re.search("^" + did + r"\s(.*)$", pcidata, re.MULTILINE)
+ if m:
+ d['host'] = m.group(1)
+
+ self.get_holders(d, sysdir)
+
+ device_facts['devices'][diskname] = d
+
+ return device_facts
+
+ def get_uptime_facts(self):
+ uptime_facts = {}
+ uptime_file_content = get_file_content('/proc/uptime')
+ if uptime_file_content:
+ uptime_seconds_string = uptime_file_content.split(' ')[0]
+ uptime_facts['uptime_seconds'] = int(float(uptime_seconds_string))
+
+ return uptime_facts
+
+ def _find_mapper_device_name(self, dm_device):
+ dm_prefix = '/dev/dm-'
+ mapper_device = dm_device
+ if dm_device.startswith(dm_prefix):
+ dmsetup_cmd = self.module.get_bin_path('dmsetup', True)
+ mapper_prefix = '/dev/mapper/'
+ rc, dm_name, err = self.module.run_command("%s info -C --noheadings -o name %s" % (dmsetup_cmd, dm_device))
+ if rc == 0:
+ mapper_device = mapper_prefix + dm_name.rstrip()
+ return mapper_device
+
+ def get_lvm_facts(self):
+ """ Get LVM Facts if running as root and lvm utils are available """
+
+ lvm_facts = {'lvm': 'N/A'}
+
+ if os.getuid() == 0 and self.module.get_bin_path('vgs'):
+ lvm_util_options = '--noheadings --nosuffix --units g --separator ,'
+
+ vgs_path = self.module.get_bin_path('vgs')
+ # vgs fields: VG #PV #LV #SN Attr VSize VFree
+ vgs = {}
+ if vgs_path:
+ rc, vg_lines, err = self.module.run_command('%s %s' % (vgs_path, lvm_util_options))
+ for vg_line in vg_lines.splitlines():
+ items = vg_line.strip().split(',')
+ vgs[items[0]] = {'size_g': items[-2],
+ 'free_g': items[-1],
+ 'num_lvs': items[2],
+ 'num_pvs': items[1]}
+
+ lvs_path = self.module.get_bin_path('lvs')
+ # lvs fields:
+ # LV VG Attr LSize Pool Origin Data% Move Log Copy% Convert
+ lvs = {}
+ if lvs_path:
+ rc, lv_lines, err = self.module.run_command('%s %s' % (lvs_path, lvm_util_options))
+ for lv_line in lv_lines.splitlines():
+ items = lv_line.strip().split(',')
+ lvs[items[0]] = {'size_g': items[3], 'vg': items[1]}
+
+ pvs_path = self.module.get_bin_path('pvs')
+ # pvs fields: PV VG #Fmt #Attr PSize PFree
+ pvs = {}
+ if pvs_path:
+ rc, pv_lines, err = self.module.run_command('%s %s' % (pvs_path, lvm_util_options))
+ for pv_line in pv_lines.splitlines():
+ items = pv_line.strip().split(',')
+ pvs[self._find_mapper_device_name(items[0])] = {
+ 'size_g': items[4],
+ 'free_g': items[5],
+ 'vg': items[1]}
+
+ lvm_facts['lvm'] = {'lvs': lvs, 'vgs': vgs, 'pvs': pvs}
+
+ return lvm_facts
+
+
+class LinuxHardwareCollector(HardwareCollector):
+ _platform = 'Linux'
+ _fact_class = LinuxHardware
+
+ required_facts = set(['platform'])
diff --git a/lib/ansible/module_utils/facts/hardware/netbsd.py b/lib/ansible/module_utils/facts/hardware/netbsd.py
new file mode 100644
index 0000000..c6557aa
--- /dev/null
+++ b/lib/ansible/module_utils/facts/hardware/netbsd.py
@@ -0,0 +1,184 @@
+# 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 re
+import time
+
+from ansible.module_utils.six.moves import reduce
+
+from ansible.module_utils.facts.hardware.base import Hardware, HardwareCollector
+from ansible.module_utils.facts.timeout import TimeoutError, timeout
+
+from ansible.module_utils.facts.utils import get_file_content, get_file_lines, get_mount_size
+from ansible.module_utils.facts.sysctl import get_sysctl
+
+
+class NetBSDHardware(Hardware):
+ """
+ NetBSD-specific subclass of Hardware. Defines memory and CPU facts:
+ - memfree_mb
+ - memtotal_mb
+ - swapfree_mb
+ - swaptotal_mb
+ - processor (a list)
+ - processor_cores
+ - processor_count
+ - devices
+ - uptime_seconds
+ """
+ platform = 'NetBSD'
+ MEMORY_FACTS = ['MemTotal', 'SwapTotal', 'MemFree', 'SwapFree']
+
+ def populate(self, collected_facts=None):
+ hardware_facts = {}
+ self.sysctl = get_sysctl(self.module, ['machdep'])
+ cpu_facts = self.get_cpu_facts()
+ memory_facts = self.get_memory_facts()
+
+ mount_facts = {}
+ try:
+ mount_facts = self.get_mount_facts()
+ except TimeoutError:
+ pass
+
+ dmi_facts = self.get_dmi_facts()
+ uptime_facts = self.get_uptime_facts()
+
+ hardware_facts.update(cpu_facts)
+ hardware_facts.update(memory_facts)
+ hardware_facts.update(mount_facts)
+ hardware_facts.update(dmi_facts)
+ hardware_facts.update(uptime_facts)
+
+ return hardware_facts
+
+ def get_cpu_facts(self):
+ cpu_facts = {}
+
+ i = 0
+ physid = 0
+ sockets = {}
+ if not os.access("/proc/cpuinfo", os.R_OK):
+ return cpu_facts
+ cpu_facts['processor'] = []
+ for line in get_file_lines("/proc/cpuinfo"):
+ data = line.split(":", 1)
+ key = data[0].strip()
+ # model name is for Intel arch, Processor (mind the uppercase P)
+ # works for some ARM devices, like the Sheevaplug.
+ if key == 'model name' or key == 'Processor':
+ if 'processor' not in cpu_facts:
+ cpu_facts['processor'] = []
+ cpu_facts['processor'].append(data[1].strip())
+ i += 1
+ elif key == 'physical id':
+ physid = data[1].strip()
+ if physid not in sockets:
+ sockets[physid] = 1
+ elif key == 'cpu cores':
+ sockets[physid] = int(data[1].strip())
+ if len(sockets) > 0:
+ cpu_facts['processor_count'] = len(sockets)
+ cpu_facts['processor_cores'] = reduce(lambda x, y: x + y, sockets.values())
+ else:
+ cpu_facts['processor_count'] = i
+ cpu_facts['processor_cores'] = 'NA'
+
+ return cpu_facts
+
+ def get_memory_facts(self):
+ memory_facts = {}
+ if not os.access("/proc/meminfo", os.R_OK):
+ return memory_facts
+ for line in get_file_lines("/proc/meminfo"):
+ data = line.split(":", 1)
+ key = data[0]
+ if key in NetBSDHardware.MEMORY_FACTS:
+ val = data[1].strip().split(' ')[0]
+ memory_facts["%s_mb" % key.lower()] = int(val) // 1024
+
+ return memory_facts
+
+ @timeout()
+ def get_mount_facts(self):
+ mount_facts = {}
+
+ mount_facts['mounts'] = []
+ fstab = get_file_content('/etc/fstab')
+
+ if not fstab:
+ return mount_facts
+
+ for line in fstab.splitlines():
+ if line.startswith('#') or line.strip() == '':
+ continue
+ fields = re.sub(r'\s+', ' ', line).split()
+ mount_statvfs_info = get_mount_size(fields[1])
+ mount_info = {'mount': fields[1],
+ 'device': fields[0],
+ 'fstype': fields[2],
+ 'options': fields[3]}
+ mount_info.update(mount_statvfs_info)
+ mount_facts['mounts'].append(mount_info)
+ return mount_facts
+
+ def get_dmi_facts(self):
+ dmi_facts = {}
+ # We don't use dmidecode(8) here because:
+ # - it would add dependency on an external package
+ # - dmidecode(8) can only be ran as root
+ # So instead we rely on sysctl(8) to provide us the information on a
+ # best-effort basis. As a bonus we also get facts on non-amd64/i386
+ # platforms this way.
+ sysctl_to_dmi = {
+ 'machdep.dmi.system-product': 'product_name',
+ 'machdep.dmi.system-version': 'product_version',
+ 'machdep.dmi.system-uuid': 'product_uuid',
+ 'machdep.dmi.system-serial': 'product_serial',
+ 'machdep.dmi.system-vendor': 'system_vendor',
+ }
+
+ for mib in sysctl_to_dmi:
+ if mib in self.sysctl:
+ dmi_facts[sysctl_to_dmi[mib]] = self.sysctl[mib]
+
+ return dmi_facts
+
+ def get_uptime_facts(self):
+ # On NetBSD, we need to call sysctl with -n to get this value as an int.
+ sysctl_cmd = self.module.get_bin_path('sysctl')
+ cmd = [sysctl_cmd, '-n', 'kern.boottime']
+
+ rc, out, err = self.module.run_command(cmd)
+
+ if rc != 0:
+ return {}
+
+ kern_boottime = out.strip()
+ if not kern_boottime.isdigit():
+ return {}
+
+ return {
+ 'uptime_seconds': int(time.time() - int(kern_boottime)),
+ }
+
+
+class NetBSDHardwareCollector(HardwareCollector):
+ _fact_class = NetBSDHardware
+ _platform = 'NetBSD'
diff --git a/lib/ansible/module_utils/facts/hardware/openbsd.py b/lib/ansible/module_utils/facts/hardware/openbsd.py
new file mode 100644
index 0000000..3bcf8ce
--- /dev/null
+++ b/lib/ansible/module_utils/facts/hardware/openbsd.py
@@ -0,0 +1,184 @@
+# 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 time
+
+from ansible.module_utils._text import to_text
+
+from ansible.module_utils.facts.hardware.base import Hardware, HardwareCollector
+from ansible.module_utils.facts import timeout
+
+from ansible.module_utils.facts.utils import get_file_content, get_mount_size
+from ansible.module_utils.facts.sysctl import get_sysctl
+
+
+class OpenBSDHardware(Hardware):
+ """
+ OpenBSD-specific subclass of Hardware. Defines memory, CPU and device facts:
+ - memfree_mb
+ - memtotal_mb
+ - swapfree_mb
+ - swaptotal_mb
+ - processor (a list)
+ - processor_cores
+ - processor_count
+ - processor_speed
+ - uptime_seconds
+
+ In addition, it also defines number of DMI facts and device facts.
+ """
+ platform = 'OpenBSD'
+
+ def populate(self, collected_facts=None):
+ hardware_facts = {}
+ self.sysctl = get_sysctl(self.module, ['hw'])
+
+ hardware_facts.update(self.get_processor_facts())
+ hardware_facts.update(self.get_memory_facts())
+ hardware_facts.update(self.get_device_facts())
+ hardware_facts.update(self.get_dmi_facts())
+ hardware_facts.update(self.get_uptime_facts())
+
+ # storage devices notorioslly prone to hang/block so they are under a timeout
+ try:
+ hardware_facts.update(self.get_mount_facts())
+ except timeout.TimeoutError:
+ pass
+
+ return hardware_facts
+
+ @timeout.timeout()
+ def get_mount_facts(self):
+ mount_facts = {}
+
+ mount_facts['mounts'] = []
+ fstab = get_file_content('/etc/fstab')
+ if fstab:
+ for line in fstab.splitlines():
+ if line.startswith('#') or line.strip() == '':
+ continue
+ fields = re.sub(r'\s+', ' ', line).split()
+ if fields[1] == 'none' or fields[3] == 'xx':
+ continue
+ mount_statvfs_info = get_mount_size(fields[1])
+ mount_info = {'mount': fields[1],
+ 'device': fields[0],
+ 'fstype': fields[2],
+ 'options': fields[3]}
+ mount_info.update(mount_statvfs_info)
+ mount_facts['mounts'].append(mount_info)
+ return mount_facts
+
+ def get_memory_facts(self):
+ memory_facts = {}
+ # Get free memory. vmstat output looks like:
+ # procs memory page disks traps cpu
+ # r b w avm fre flt re pi po fr sr wd0 fd0 int sys cs us sy id
+ # 0 0 0 47512 28160 51 0 0 0 0 0 1 0 116 89 17 0 1 99
+ rc, out, err = self.module.run_command("/usr/bin/vmstat")
+ if rc == 0:
+ memory_facts['memfree_mb'] = int(out.splitlines()[-1].split()[4]) // 1024
+ memory_facts['memtotal_mb'] = int(self.sysctl['hw.usermem']) // 1024 // 1024
+
+ # Get swapctl info. swapctl output looks like:
+ # total: 69268 1K-blocks allocated, 0 used, 69268 available
+ # And for older OpenBSD:
+ # total: 69268k bytes allocated = 0k used, 69268k available
+ rc, out, err = self.module.run_command("/sbin/swapctl -sk")
+ if rc == 0:
+ swaptrans = {ord(u'k'): None,
+ ord(u'm'): None,
+ ord(u'g'): None}
+ data = to_text(out, errors='surrogate_or_strict').split()
+ memory_facts['swapfree_mb'] = int(data[-2].translate(swaptrans)) // 1024
+ memory_facts['swaptotal_mb'] = int(data[1].translate(swaptrans)) // 1024
+
+ return memory_facts
+
+ def get_uptime_facts(self):
+ # On openbsd, we need to call it with -n to get this value as an int.
+ sysctl_cmd = self.module.get_bin_path('sysctl')
+ cmd = [sysctl_cmd, '-n', 'kern.boottime']
+
+ rc, out, err = self.module.run_command(cmd)
+
+ if rc != 0:
+ return {}
+
+ kern_boottime = out.strip()
+ if not kern_boottime.isdigit():
+ return {}
+
+ return {
+ 'uptime_seconds': int(time.time() - int(kern_boottime)),
+ }
+
+ def get_processor_facts(self):
+ cpu_facts = {}
+ processor = []
+ for i in range(int(self.sysctl['hw.ncpuonline'])):
+ processor.append(self.sysctl['hw.model'])
+
+ cpu_facts['processor'] = processor
+ # The following is partly a lie because there is no reliable way to
+ # determine the number of physical CPUs in the system. We can only
+ # query the number of logical CPUs, which hides the number of cores.
+ # On amd64/i386 we could try to inspect the smt/core/package lines in
+ # dmesg, however even those have proven to be unreliable.
+ # So take a shortcut and report the logical number of processors in
+ # 'processor_count' and 'processor_cores' and leave it at that.
+ cpu_facts['processor_count'] = self.sysctl['hw.ncpuonline']
+ cpu_facts['processor_cores'] = self.sysctl['hw.ncpuonline']
+
+ return cpu_facts
+
+ def get_device_facts(self):
+ device_facts = {}
+ devices = []
+ devices.extend(self.sysctl['hw.disknames'].split(','))
+ device_facts['devices'] = devices
+
+ return device_facts
+
+ def get_dmi_facts(self):
+ dmi_facts = {}
+ # We don't use dmidecode(8) here because:
+ # - it would add dependency on an external package
+ # - dmidecode(8) can only be ran as root
+ # So instead we rely on sysctl(8) to provide us the information on a
+ # best-effort basis. As a bonus we also get facts on non-amd64/i386
+ # platforms this way.
+ sysctl_to_dmi = {
+ 'hw.product': 'product_name',
+ 'hw.version': 'product_version',
+ 'hw.uuid': 'product_uuid',
+ 'hw.serialno': 'product_serial',
+ 'hw.vendor': 'system_vendor',
+ }
+
+ for mib in sysctl_to_dmi:
+ if mib in self.sysctl:
+ dmi_facts[sysctl_to_dmi[mib]] = self.sysctl[mib]
+
+ return dmi_facts
+
+
+class OpenBSDHardwareCollector(HardwareCollector):
+ _fact_class = OpenBSDHardware
+ _platform = 'OpenBSD'
diff --git a/lib/ansible/module_utils/facts/hardware/sunos.py b/lib/ansible/module_utils/facts/hardware/sunos.py
new file mode 100644
index 0000000..0a77db0
--- /dev/null
+++ b/lib/ansible/module_utils/facts/hardware/sunos.py
@@ -0,0 +1,286 @@
+# 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 time
+
+from ansible.module_utils.common.locale import get_best_parsable_locale
+from ansible.module_utils.common.text.formatters import bytes_to_human
+from ansible.module_utils.facts.utils import get_file_content, get_mount_size
+from ansible.module_utils.facts.hardware.base import Hardware, HardwareCollector
+from ansible.module_utils.facts import timeout
+from ansible.module_utils.six.moves import reduce
+
+
+class SunOSHardware(Hardware):
+ """
+ In addition to the generic memory and cpu facts, this also sets
+ swap_reserved_mb and swap_allocated_mb that is available from *swap -s*.
+ """
+ platform = 'SunOS'
+
+ def populate(self, collected_facts=None):
+ hardware_facts = {}
+
+ # FIXME: could pass to run_command(environ_update), but it also tweaks the env
+ # of the parent process instead of altering an env provided to Popen()
+ # Use C locale for hardware collection helpers to avoid locale specific number formatting (#24542)
+ locale = get_best_parsable_locale(self.module)
+ self.module.run_command_environ_update = {'LANG': locale, 'LC_ALL': locale, 'LC_NUMERIC': locale}
+
+ cpu_facts = self.get_cpu_facts()
+ memory_facts = self.get_memory_facts()
+ dmi_facts = self.get_dmi_facts()
+ device_facts = self.get_device_facts()
+ uptime_facts = self.get_uptime_facts()
+
+ mount_facts = {}
+ try:
+ mount_facts = self.get_mount_facts()
+ except timeout.TimeoutError:
+ pass
+
+ hardware_facts.update(cpu_facts)
+ hardware_facts.update(memory_facts)
+ hardware_facts.update(dmi_facts)
+ hardware_facts.update(device_facts)
+ hardware_facts.update(uptime_facts)
+ hardware_facts.update(mount_facts)
+
+ return hardware_facts
+
+ def get_cpu_facts(self, collected_facts=None):
+ physid = 0
+ sockets = {}
+
+ cpu_facts = {}
+ collected_facts = collected_facts or {}
+
+ rc, out, err = self.module.run_command("/usr/bin/kstat cpu_info")
+
+ cpu_facts['processor'] = []
+
+ for line in out.splitlines():
+ if len(line) < 1:
+ continue
+
+ data = line.split(None, 1)
+ key = data[0].strip()
+
+ # "brand" works on Solaris 10 & 11. "implementation" for Solaris 9.
+ if key == 'module:':
+ brand = ''
+ elif key == 'brand':
+ brand = data[1].strip()
+ elif key == 'clock_MHz':
+ clock_mhz = data[1].strip()
+ elif key == 'implementation':
+ processor = brand or data[1].strip()
+ # Add clock speed to description for SPARC CPU
+ # FIXME
+ if collected_facts.get('ansible_machine') != 'i86pc':
+ processor += " @ " + clock_mhz + "MHz"
+ if 'ansible_processor' not in collected_facts:
+ cpu_facts['processor'] = []
+ cpu_facts['processor'].append(processor)
+ elif key == 'chip_id':
+ physid = data[1].strip()
+ if physid not in sockets:
+ sockets[physid] = 1
+ else:
+ sockets[physid] += 1
+
+ # Counting cores on Solaris can be complicated.
+ # https://blogs.oracle.com/mandalika/entry/solaris_show_me_the_cpu
+ # Treat 'processor_count' as physical sockets and 'processor_cores' as
+ # virtual CPUs visisble to Solaris. Not a true count of cores for modern SPARC as
+ # these processors have: sockets -> cores -> threads/virtual CPU.
+ if len(sockets) > 0:
+ cpu_facts['processor_count'] = len(sockets)
+ cpu_facts['processor_cores'] = reduce(lambda x, y: x + y, sockets.values())
+ else:
+ cpu_facts['processor_cores'] = 'NA'
+ cpu_facts['processor_count'] = len(cpu_facts['processor'])
+
+ return cpu_facts
+
+ def get_memory_facts(self):
+ memory_facts = {}
+
+ rc, out, err = self.module.run_command(["/usr/sbin/prtconf"])
+
+ for line in out.splitlines():
+ if 'Memory size' in line:
+ memory_facts['memtotal_mb'] = int(line.split()[2])
+
+ rc, out, err = self.module.run_command("/usr/sbin/swap -s")
+
+ allocated = int(out.split()[1][:-1])
+ reserved = int(out.split()[5][:-1])
+ used = int(out.split()[8][:-1])
+ free = int(out.split()[10][:-1])
+
+ memory_facts['swapfree_mb'] = free // 1024
+ memory_facts['swaptotal_mb'] = (free + used) // 1024
+ memory_facts['swap_allocated_mb'] = allocated // 1024
+ memory_facts['swap_reserved_mb'] = reserved // 1024
+
+ return memory_facts
+
+ @timeout.timeout()
+ def get_mount_facts(self):
+ mount_facts = {}
+ mount_facts['mounts'] = []
+
+ # For a detailed format description see mnttab(4)
+ # special mount_point fstype options time
+ fstab = get_file_content('/etc/mnttab')
+
+ if fstab:
+ for line in fstab.splitlines():
+ fields = line.split('\t')
+ mount_statvfs_info = get_mount_size(fields[1])
+ mount_info = {'mount': fields[1],
+ 'device': fields[0],
+ 'fstype': fields[2],
+ 'options': fields[3],
+ 'time': fields[4]}
+ mount_info.update(mount_statvfs_info)
+ mount_facts['mounts'].append(mount_info)
+
+ return mount_facts
+
+ def get_dmi_facts(self):
+ dmi_facts = {}
+
+ # On Solaris 8 the prtdiag wrapper is absent from /usr/sbin,
+ # but that's okay, because we know where to find the real thing:
+ rc, platform, err = self.module.run_command('/usr/bin/uname -i')
+ platform_sbin = '/usr/platform/' + platform.rstrip() + '/sbin'
+
+ prtdiag_path = self.module.get_bin_path("prtdiag", opt_dirs=[platform_sbin])
+ rc, out, err = self.module.run_command(prtdiag_path)
+ """
+ rc returns 1
+ """
+ if out:
+ system_conf = out.split('\n')[0]
+
+ # If you know of any other manufacturers whose names appear in
+ # the first line of prtdiag's output, please add them here:
+ vendors = [
+ "Fujitsu",
+ "Oracle Corporation",
+ "QEMU",
+ "Sun Microsystems",
+ "VMware, Inc.",
+ ]
+ vendor_regexp = "|".join(map(re.escape, vendors))
+ system_conf_regexp = (r'System Configuration:\s+'
+ + r'(' + vendor_regexp + r')\s+'
+ + r'(?:sun\w+\s+)?'
+ + r'(.+)')
+
+ found = re.match(system_conf_regexp, system_conf)
+ if found:
+ dmi_facts['system_vendor'] = found.group(1)
+ dmi_facts['product_name'] = found.group(2)
+
+ return dmi_facts
+
+ def get_device_facts(self):
+ # Device facts are derived for sdderr kstats. This code does not use the
+ # full output, but rather queries for specific stats.
+ # Example output:
+ # sderr:0:sd0,err:Hard Errors 0
+ # sderr:0:sd0,err:Illegal Request 6
+ # sderr:0:sd0,err:Media Error 0
+ # sderr:0:sd0,err:Predictive Failure Analysis 0
+ # sderr:0:sd0,err:Product VBOX HARDDISK 9
+ # sderr:0:sd0,err:Revision 1.0
+ # sderr:0:sd0,err:Serial No VB0ad2ec4d-074a
+ # sderr:0:sd0,err:Size 53687091200
+ # sderr:0:sd0,err:Soft Errors 0
+ # sderr:0:sd0,err:Transport Errors 0
+ # sderr:0:sd0,err:Vendor ATA
+
+ device_facts = {}
+ device_facts['devices'] = {}
+
+ disk_stats = {
+ 'Product': 'product',
+ 'Revision': 'revision',
+ 'Serial No': 'serial',
+ 'Size': 'size',
+ 'Vendor': 'vendor',
+ 'Hard Errors': 'hard_errors',
+ 'Soft Errors': 'soft_errors',
+ 'Transport Errors': 'transport_errors',
+ 'Media Error': 'media_errors',
+ 'Predictive Failure Analysis': 'predictive_failure_analysis',
+ 'Illegal Request': 'illegal_request',
+ }
+
+ cmd = ['/usr/bin/kstat', '-p']
+
+ for ds in disk_stats:
+ cmd.append('sderr:::%s' % ds)
+
+ d = {}
+ rc, out, err = self.module.run_command(cmd)
+ if rc != 0:
+ return device_facts
+
+ sd_instances = frozenset(line.split(':')[1] for line in out.split('\n') if line.startswith('sderr'))
+ for instance in sd_instances:
+ lines = (line for line in out.split('\n') if ':' in line and line.split(':')[1] == instance)
+ for line in lines:
+ text, value = line.split('\t')
+ stat = text.split(':')[3]
+
+ if stat == 'Size':
+ d[disk_stats.get(stat)] = bytes_to_human(float(value))
+ else:
+ d[disk_stats.get(stat)] = value.rstrip()
+
+ diskname = 'sd' + instance
+ device_facts['devices'][diskname] = d
+ d = {}
+
+ return device_facts
+
+ def get_uptime_facts(self):
+ uptime_facts = {}
+ # sample kstat output:
+ # unix:0:system_misc:boot_time 1548249689
+ rc, out, err = self.module.run_command('/usr/bin/kstat -p unix:0:system_misc:boot_time')
+
+ if rc != 0:
+ return
+
+ # uptime = $current_time - $boot_time
+ uptime_facts['uptime_seconds'] = int(time.time() - int(out.split('\t')[1]))
+
+ return uptime_facts
+
+
+class SunOSHardwareCollector(HardwareCollector):
+ _fact_class = SunOSHardware
+ _platform = 'SunOS'
+
+ required_facts = set(['platform'])
diff --git a/lib/ansible/module_utils/facts/namespace.py b/lib/ansible/module_utils/facts/namespace.py
new file mode 100644
index 0000000..2d6bf8a
--- /dev/null
+++ b/lib/ansible/module_utils/facts/namespace.py
@@ -0,0 +1,51 @@
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# (c) 2017 Red Hat Inc.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class FactNamespace:
+ def __init__(self, namespace_name):
+ self.namespace_name = namespace_name
+
+ def transform(self, name):
+ '''Take a text name, and transforms it as needed (add a namespace prefix, etc)'''
+ return name
+
+ def _underscore(self, name):
+ return name.replace('-', '_')
+
+
+class PrefixFactNamespace(FactNamespace):
+ def __init__(self, namespace_name, prefix=None):
+ super(PrefixFactNamespace, self).__init__(namespace_name)
+ self.prefix = prefix
+
+ def transform(self, name):
+ new_name = self._underscore(name)
+ return '%s%s' % (self.prefix, new_name)
diff --git a/lib/ansible/module_utils/facts/network/__init__.py b/lib/ansible/module_utils/facts/network/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/__init__.py
diff --git a/lib/ansible/module_utils/facts/network/aix.py b/lib/ansible/module_utils/facts/network/aix.py
new file mode 100644
index 0000000..e9c90c6
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/aix.py
@@ -0,0 +1,145 @@
+# 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
+
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
+
+
+class AIXNetwork(GenericBsdIfconfigNetwork):
+ """
+ This is the AIX Network Class.
+ It uses the GenericBsdIfconfigNetwork unchanged.
+ """
+ platform = 'AIX'
+
+ def get_default_interfaces(self, route_path):
+ interface = dict(v4={}, v6={})
+
+ netstat_path = self.module.get_bin_path('netstat')
+
+ if netstat_path:
+ rc, out, err = self.module.run_command([netstat_path, '-nr'])
+
+ lines = out.splitlines()
+ for line in lines:
+ words = line.split()
+ if len(words) > 1 and words[0] == 'default':
+ if '.' in words[1]:
+ interface['v4']['gateway'] = words[1]
+ interface['v4']['interface'] = words[5]
+ elif ':' in words[1]:
+ interface['v6']['gateway'] = words[1]
+ interface['v6']['interface'] = words[5]
+
+ return interface['v4'], interface['v6']
+
+ # AIX 'ifconfig -a' does not have three words in the interface line
+ def get_interfaces_info(self, ifconfig_path, ifconfig_options='-a'):
+ interfaces = {}
+ current_if = {}
+ ips = dict(
+ all_ipv4_addresses=[],
+ all_ipv6_addresses=[],
+ )
+
+ uname_rc = None
+ uname_out = None
+ uname_err = None
+ uname_path = self.module.get_bin_path('uname')
+ if uname_path:
+ uname_rc, uname_out, uname_err = self.module.run_command([uname_path, '-W'])
+
+ rc, out, err = self.module.run_command([ifconfig_path, ifconfig_options])
+
+ for line in out.splitlines():
+
+ if line:
+ words = line.split()
+
+ # only this condition differs from GenericBsdIfconfigNetwork
+ if re.match(r'^\w*\d*:', line):
+ current_if = self.parse_interface_line(words)
+ interfaces[current_if['device']] = current_if
+ elif words[0].startswith('options='):
+ self.parse_options_line(words, current_if, ips)
+ elif words[0] == 'nd6':
+ self.parse_nd6_line(words, current_if, ips)
+ elif words[0] == 'ether':
+ self.parse_ether_line(words, current_if, ips)
+ elif words[0] == 'media:':
+ self.parse_media_line(words, current_if, ips)
+ elif words[0] == 'status:':
+ self.parse_status_line(words, current_if, ips)
+ elif words[0] == 'lladdr':
+ self.parse_lladdr_line(words, current_if, ips)
+ elif words[0] == 'inet':
+ self.parse_inet_line(words, current_if, ips)
+ elif words[0] == 'inet6':
+ self.parse_inet6_line(words, current_if, ips)
+ else:
+ self.parse_unknown_line(words, current_if, ips)
+
+ # don't bother with wpars it does not work
+ # zero means not in wpar
+ if not uname_rc and uname_out.split()[0] == '0':
+
+ if current_if['macaddress'] == 'unknown' and re.match('^en', current_if['device']):
+ entstat_path = self.module.get_bin_path('entstat')
+ if entstat_path:
+ rc, out, err = self.module.run_command([entstat_path, current_if['device']])
+ if rc != 0:
+ break
+ for line in out.splitlines():
+ if not line:
+ pass
+ buff = re.match('^Hardware Address: (.*)', line)
+ if buff:
+ current_if['macaddress'] = buff.group(1)
+
+ buff = re.match('^Device Type:', line)
+ if buff and re.match('.*Ethernet', line):
+ current_if['type'] = 'ether'
+
+ # device must have mtu attribute in ODM
+ if 'mtu' not in current_if:
+ lsattr_path = self.module.get_bin_path('lsattr')
+ if lsattr_path:
+ rc, out, err = self.module.run_command([lsattr_path, '-El', current_if['device']])
+ if rc != 0:
+ break
+ for line in out.splitlines():
+ if line:
+ words = line.split()
+ if words[0] == 'mtu':
+ current_if['mtu'] = words[1]
+ return interfaces, ips
+
+ # AIX 'ifconfig -a' does not inform about MTU, so remove current_if['mtu'] here
+ def parse_interface_line(self, words):
+ device = words[0][0:-1]
+ current_if = {'device': device, 'ipv4': [], 'ipv6': [], 'type': 'unknown'}
+ current_if['flags'] = self.get_options(words[1])
+ current_if['macaddress'] = 'unknown' # will be overwritten later
+ return current_if
+
+
+class AIXNetworkCollector(NetworkCollector):
+ _fact_class = AIXNetwork
+ _platform = 'AIX'
diff --git a/lib/ansible/module_utils/facts/network/base.py b/lib/ansible/module_utils/facts/network/base.py
new file mode 100644
index 0000000..8243f06
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/base.py
@@ -0,0 +1,72 @@
+# 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
+
+
+class Network:
+ """
+ This is a generic Network subclass of Facts. This should be further
+ subclassed to implement per platform. If you subclass this,
+ you must define:
+ - interfaces (a list of interface names)
+ - interface_<name> dictionary of ipv4, ipv6, and mac address information.
+
+ All subclasses MUST define platform.
+ """
+ platform = 'Generic'
+
+ # FIXME: remove load_on_init when we can
+ def __init__(self, module, load_on_init=False):
+ self.module = module
+
+ # TODO: more or less abstract/NotImplemented
+ def populate(self, collected_facts=None):
+ return {}
+
+
+class NetworkCollector(BaseFactCollector):
+ # MAYBE: we could try to build this based on the arch specific implementation of Network() or its kin
+ name = 'network'
+ _fact_class = Network
+ _fact_ids = set(['interfaces',
+ 'default_ipv4',
+ 'default_ipv6',
+ 'all_ipv4_addresses',
+ 'all_ipv6_addresses']) # type: t.Set[str]
+
+ IPV6_SCOPE = {'0': 'global',
+ '10': 'host',
+ '20': 'link',
+ '40': 'admin',
+ '50': 'site',
+ '80': 'organization'}
+
+ def collect(self, module=None, collected_facts=None):
+ collected_facts = collected_facts or {}
+ if not module:
+ return {}
+
+ # Network munges cached_facts by side effect, so give it a copy
+ facts_obj = self._fact_class(module)
+
+ facts_dict = facts_obj.populate(collected_facts=collected_facts)
+
+ return facts_dict
diff --git a/lib/ansible/module_utils/facts/network/darwin.py b/lib/ansible/module_utils/facts/network/darwin.py
new file mode 100644
index 0000000..90117e5
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/darwin.py
@@ -0,0 +1,49 @@
+# 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
+
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
+
+
+class DarwinNetwork(GenericBsdIfconfigNetwork):
+ """
+ This is the Mac macOS Darwin Network Class.
+ It uses the GenericBsdIfconfigNetwork unchanged
+ """
+ platform = 'Darwin'
+
+ # media line is different to the default FreeBSD one
+ def parse_media_line(self, words, current_if, ips):
+ # not sure if this is useful - we also drop information
+ current_if['media'] = 'Unknown' # Mac does not give us this
+ current_if['media_select'] = words[1]
+ if len(words) > 2:
+ # MacOSX sets the media to '<unknown type>' for bridge interface
+ # and parsing splits this into two words; this if/else helps
+ if words[1] == '<unknown' and words[2] == 'type>':
+ current_if['media_select'] = 'Unknown'
+ current_if['media_type'] = 'unknown type'
+ else:
+ current_if['media_type'] = words[2][1:-1]
+ if len(words) > 3:
+ current_if['media_options'] = self.get_options(words[3])
+
+
+class DarwinNetworkCollector(NetworkCollector):
+ _fact_class = DarwinNetwork
+ _platform = 'Darwin'
diff --git a/lib/ansible/module_utils/facts/network/dragonfly.py b/lib/ansible/module_utils/facts/network/dragonfly.py
new file mode 100644
index 0000000..e43bbb2
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/dragonfly.py
@@ -0,0 +1,33 @@
+# 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
+
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
+
+
+class DragonFlyNetwork(GenericBsdIfconfigNetwork):
+ """
+ This is the DragonFly Network Class.
+ It uses the GenericBsdIfconfigNetwork unchanged.
+ """
+ platform = 'DragonFly'
+
+
+class DragonFlyNetworkCollector(NetworkCollector):
+ _fact_class = DragonFlyNetwork
+ _platform = 'DragonFly'
diff --git a/lib/ansible/module_utils/facts/network/fc_wwn.py b/lib/ansible/module_utils/facts/network/fc_wwn.py
new file mode 100644
index 0000000..86182f8
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/fc_wwn.py
@@ -0,0 +1,111 @@
+# Fibre Channel WWN initiator 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 sys
+import glob
+
+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 FcWwnInitiatorFactCollector(BaseFactCollector):
+ name = 'fibre_channel_wwn'
+ _fact_ids = set() # type: t.Set[str]
+
+ def collect(self, module=None, collected_facts=None):
+ """
+ Example contents /sys/class/fc_host/*/port_name:
+
+ 0x21000014ff52a9bb
+
+ """
+
+ fc_facts = {}
+ fc_facts['fibre_channel_wwn'] = []
+ if sys.platform.startswith('linux'):
+ for fcfile in glob.glob('/sys/class/fc_host/*/port_name'):
+ for line in get_file_lines(fcfile):
+ fc_facts['fibre_channel_wwn'].append(line.rstrip()[2:])
+ elif sys.platform.startswith('sunos'):
+ """
+ on solaris 10 or solaris 11 should use `fcinfo hba-port`
+ TBD (not implemented): on solaris 9 use `prtconf -pv`
+ """
+ cmd = module.get_bin_path('fcinfo')
+ if cmd:
+ cmd = cmd + " hba-port"
+ rc, fcinfo_out, err = module.run_command(cmd)
+ """
+ # fcinfo hba-port | grep "Port WWN"
+ HBA Port WWN: 10000090fa1658de
+ """
+ if rc == 0 and fcinfo_out:
+ for line in fcinfo_out.splitlines():
+ if 'Port WWN' in line:
+ data = line.split(' ')
+ fc_facts['fibre_channel_wwn'].append(data[-1].rstrip())
+ elif sys.platform.startswith('aix'):
+ cmd = module.get_bin_path('lsdev')
+ lscfg_cmd = module.get_bin_path('lscfg')
+ if cmd and lscfg_cmd:
+ # get list of available fibre-channel devices (fcs)
+ cmd = cmd + " -Cc adapter -l fcs*"
+ rc, lsdev_out, err = module.run_command(cmd)
+ if rc == 0 and lsdev_out:
+ for line in lsdev_out.splitlines():
+ # if device is available (not in defined state), get its WWN
+ if 'Available' in line:
+ data = line.split(' ')
+ cmd = lscfg_cmd + " -vl %s" % data[0]
+ rc, lscfg_out, err = module.run_command(cmd)
+ # example output
+ # lscfg -vpl fcs3 | grep "Network Address"
+ # Network Address.............10000090FA551509
+ if rc == 0 and lscfg_out:
+ for line in lscfg_out.splitlines():
+ if 'Network Address' in line:
+ data = line.split('.')
+ fc_facts['fibre_channel_wwn'].append(data[-1].rstrip())
+ elif sys.platform.startswith('hp-ux'):
+ cmd = module.get_bin_path('ioscan')
+ fcmsu_cmd = module.get_bin_path('fcmsutil', opt_dirs=['/opt/fcms/bin'])
+ # go ahead if we have both commands available
+ if cmd and fcmsu_cmd:
+ # ioscan / get list of available fibre-channel devices (fcd)
+ cmd = cmd + " -fnC FC"
+ rc, ioscan_out, err = module.run_command(cmd)
+ if rc == 0 and ioscan_out:
+ for line in ioscan_out.splitlines():
+ line = line.strip()
+ if '/dev/fcd' in line:
+ dev = line.split(' ')
+ # get device information
+ cmd = fcmsu_cmd + " %s" % dev[0]
+ rc, fcmsutil_out, err = module.run_command(cmd)
+ # lookup the following line
+ # N_Port Port World Wide Name = 0x50060b00006975ec
+ if rc == 0 and fcmsutil_out:
+ for line in fcmsutil_out.splitlines():
+ if 'N_Port Port World Wide Name' in line:
+ data = line.split('=')
+ fc_facts['fibre_channel_wwn'].append(data[-1].strip())
+ return fc_facts
diff --git a/lib/ansible/module_utils/facts/network/freebsd.py b/lib/ansible/module_utils/facts/network/freebsd.py
new file mode 100644
index 0000000..36f6eec
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/freebsd.py
@@ -0,0 +1,33 @@
+# 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
+
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
+
+
+class FreeBSDNetwork(GenericBsdIfconfigNetwork):
+ """
+ This is the FreeBSD Network Class.
+ It uses the GenericBsdIfconfigNetwork unchanged.
+ """
+ platform = 'FreeBSD'
+
+
+class FreeBSDNetworkCollector(NetworkCollector):
+ _fact_class = FreeBSDNetwork
+ _platform = 'FreeBSD'
diff --git a/lib/ansible/module_utils/facts/network/generic_bsd.py b/lib/ansible/module_utils/facts/network/generic_bsd.py
new file mode 100644
index 0000000..8d640f2
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/generic_bsd.py
@@ -0,0 +1,321 @@
+# 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 struct
+
+from ansible.module_utils.facts.network.base import Network
+
+
+class GenericBsdIfconfigNetwork(Network):
+ """
+ This is a generic BSD subclass of Network using the ifconfig command.
+ It defines
+ - interfaces (a list of interface names)
+ - interface_<name> dictionary of ipv4, ipv6, and mac address information.
+ - all_ipv4_addresses and all_ipv6_addresses: lists of all configured addresses.
+ """
+ platform = 'Generic_BSD_Ifconfig'
+
+ def populate(self, collected_facts=None):
+ network_facts = {}
+ ifconfig_path = self.module.get_bin_path('ifconfig')
+
+ if ifconfig_path is None:
+ return network_facts
+
+ route_path = self.module.get_bin_path('route')
+
+ if route_path is None:
+ return network_facts
+
+ default_ipv4, default_ipv6 = self.get_default_interfaces(route_path)
+ interfaces, ips = self.get_interfaces_info(ifconfig_path)
+ interfaces = self.detect_type_media(interfaces)
+
+ self.merge_default_interface(default_ipv4, interfaces, 'ipv4')
+ self.merge_default_interface(default_ipv6, interfaces, 'ipv6')
+ network_facts['interfaces'] = sorted(list(interfaces.keys()))
+
+ for iface in interfaces:
+ network_facts[iface] = interfaces[iface]
+
+ network_facts['default_ipv4'] = default_ipv4
+ network_facts['default_ipv6'] = default_ipv6
+ network_facts['all_ipv4_addresses'] = ips['all_ipv4_addresses']
+ network_facts['all_ipv6_addresses'] = ips['all_ipv6_addresses']
+
+ return network_facts
+
+ def detect_type_media(self, interfaces):
+ for iface in interfaces:
+ if 'media' in interfaces[iface]:
+ if 'ether' in interfaces[iface]['media'].lower():
+ interfaces[iface]['type'] = 'ether'
+ return interfaces
+
+ def get_default_interfaces(self, route_path):
+
+ # Use the commands:
+ # route -n get default
+ # route -n get -inet6 default
+ # to find out the default outgoing interface, address, and gateway
+
+ command = dict(v4=[route_path, '-n', 'get', 'default'],
+ v6=[route_path, '-n', 'get', '-inet6', 'default'])
+
+ interface = dict(v4={}, v6={})
+
+ for v in 'v4', 'v6':
+
+ if v == 'v6' and not socket.has_ipv6:
+ continue
+ rc, out, err = self.module.run_command(command[v])
+ if not out:
+ # v6 routing may result in
+ # RTNETLINK answers: Invalid argument
+ continue
+ for line in out.splitlines():
+ words = line.strip().split(': ')
+ # Collect output from route command
+ if len(words) > 1:
+ if words[0] == 'interface':
+ interface[v]['interface'] = words[1]
+ if words[0] == 'gateway':
+ interface[v]['gateway'] = words[1]
+ # help pick the right interface address on OpenBSD
+ if words[0] == 'if address':
+ interface[v]['address'] = words[1]
+ # help pick the right interface address on NetBSD
+ if words[0] == 'local addr':
+ interface[v]['address'] = words[1]
+
+ return interface['v4'], interface['v6']
+
+ def get_interfaces_info(self, ifconfig_path, ifconfig_options='-a'):
+ interfaces = {}
+ current_if = {}
+ ips = dict(
+ all_ipv4_addresses=[],
+ all_ipv6_addresses=[],
+ )
+ # FreeBSD, DragonflyBSD, NetBSD, OpenBSD and macOS all implicitly add '-a'
+ # when running the command 'ifconfig'.
+ # Solaris must explicitly run the command 'ifconfig -a'.
+ rc, out, err = self.module.run_command([ifconfig_path, ifconfig_options])
+
+ for line in out.splitlines():
+
+ if line:
+ words = line.split()
+
+ if words[0] == 'pass':
+ continue
+ elif re.match(r'^\S', line) and len(words) > 3:
+ current_if = self.parse_interface_line(words)
+ interfaces[current_if['device']] = current_if
+ elif words[0].startswith('options='):
+ self.parse_options_line(words, current_if, ips)
+ elif words[0] == 'nd6':
+ self.parse_nd6_line(words, current_if, ips)
+ elif words[0] == 'ether':
+ self.parse_ether_line(words, current_if, ips)
+ elif words[0] == 'media:':
+ self.parse_media_line(words, current_if, ips)
+ elif words[0] == 'status:':
+ self.parse_status_line(words, current_if, ips)
+ elif words[0] == 'lladdr':
+ self.parse_lladdr_line(words, current_if, ips)
+ elif words[0] == 'inet':
+ self.parse_inet_line(words, current_if, ips)
+ elif words[0] == 'inet6':
+ self.parse_inet6_line(words, current_if, ips)
+ elif words[0] == 'tunnel':
+ self.parse_tunnel_line(words, current_if, ips)
+ else:
+ self.parse_unknown_line(words, current_if, ips)
+
+ return interfaces, ips
+
+ def parse_interface_line(self, words):
+ device = words[0][0:-1]
+ current_if = {'device': device, 'ipv4': [], 'ipv6': [], 'type': 'unknown'}
+ current_if['flags'] = self.get_options(words[1])
+ if 'LOOPBACK' in current_if['flags']:
+ current_if['type'] = 'loopback'
+ current_if['macaddress'] = 'unknown' # will be overwritten later
+
+ if len(words) >= 5: # Newer FreeBSD versions
+ current_if['metric'] = words[3]
+ current_if['mtu'] = words[5]
+ else:
+ current_if['mtu'] = words[3]
+
+ return current_if
+
+ def parse_options_line(self, words, current_if, ips):
+ # Mac has options like this...
+ current_if['options'] = self.get_options(words[0])
+
+ def parse_nd6_line(self, words, current_if, ips):
+ # FreeBSD has options like this...
+ current_if['options'] = self.get_options(words[1])
+
+ def parse_ether_line(self, words, current_if, ips):
+ current_if['macaddress'] = words[1]
+ current_if['type'] = 'ether'
+
+ def parse_media_line(self, words, current_if, ips):
+ # not sure if this is useful - we also drop information
+ current_if['media'] = words[1]
+ if len(words) > 2:
+ current_if['media_select'] = words[2]
+ if len(words) > 3:
+ current_if['media_type'] = words[3][1:]
+ if len(words) > 4:
+ current_if['media_options'] = self.get_options(words[4])
+
+ def parse_status_line(self, words, current_if, ips):
+ current_if['status'] = words[1]
+
+ def parse_lladdr_line(self, words, current_if, ips):
+ current_if['lladdr'] = words[1]
+
+ def parse_inet_line(self, words, current_if, ips):
+ # netbsd show aliases like this
+ # lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 33184
+ # inet 127.0.0.1 netmask 0xff000000
+ # inet alias 127.1.1.1 netmask 0xff000000
+ if words[1] == 'alias':
+ del words[1]
+
+ address = {'address': words[1]}
+ # cidr style ip address (eg, 127.0.0.1/24) in inet line
+ # used in netbsd ifconfig -e output after 7.1
+ if '/' in address['address']:
+ ip_address, cidr_mask = address['address'].split('/')
+
+ address['address'] = ip_address
+
+ netmask_length = int(cidr_mask)
+ netmask_bin = (1 << 32) - (1 << 32 >> int(netmask_length))
+ address['netmask'] = socket.inet_ntoa(struct.pack('!L', netmask_bin))
+
+ if len(words) > 5:
+ address['broadcast'] = words[3]
+
+ else:
+ # Don't just assume columns, use "netmask" as the index for the prior column
+ try:
+ netmask_idx = words.index('netmask') + 1
+ except ValueError:
+ netmask_idx = 3
+
+ # deal with hex netmask
+ if re.match('([0-9a-f]){8}$', words[netmask_idx]):
+ netmask = '0x' + words[netmask_idx]
+ else:
+ netmask = words[netmask_idx]
+
+ if netmask.startswith('0x'):
+ address['netmask'] = socket.inet_ntoa(struct.pack('!L', int(netmask, base=16)))
+ else:
+ # otherwise assume this is a dotted quad
+ address['netmask'] = netmask
+ # calculate the network
+ address_bin = struct.unpack('!L', socket.inet_aton(address['address']))[0]
+ netmask_bin = struct.unpack('!L', socket.inet_aton(address['netmask']))[0]
+ address['network'] = socket.inet_ntoa(struct.pack('!L', address_bin & netmask_bin))
+ if 'broadcast' not in address:
+ # broadcast may be given or we need to calculate
+ try:
+ broadcast_idx = words.index('broadcast') + 1
+ except ValueError:
+ address['broadcast'] = socket.inet_ntoa(struct.pack('!L', address_bin | (~netmask_bin & 0xffffffff)))
+ else:
+ address['broadcast'] = words[broadcast_idx]
+
+ # add to our list of addresses
+ if not words[1].startswith('127.'):
+ ips['all_ipv4_addresses'].append(address['address'])
+ current_if['ipv4'].append(address)
+
+ def parse_inet6_line(self, words, current_if, ips):
+ address = {'address': words[1]}
+
+ # using cidr style addresses, ala NetBSD ifconfig post 7.1
+ if '/' in address['address']:
+ ip_address, cidr_mask = address['address'].split('/')
+
+ address['address'] = ip_address
+ address['prefix'] = cidr_mask
+
+ if len(words) > 5:
+ address['scope'] = words[5]
+ else:
+ if (len(words) >= 4) and (words[2] == 'prefixlen'):
+ address['prefix'] = words[3]
+ if (len(words) >= 6) and (words[4] == 'scopeid'):
+ address['scope'] = words[5]
+
+ localhost6 = ['::1', '::1/128', 'fe80::1%lo0']
+ if address['address'] not in localhost6:
+ ips['all_ipv6_addresses'].append(address['address'])
+ current_if['ipv6'].append(address)
+
+ def parse_tunnel_line(self, words, current_if, ips):
+ current_if['type'] = 'tunnel'
+
+ def parse_unknown_line(self, words, current_if, ips):
+ # we are going to ignore unknown lines here - this may be
+ # a bad idea - but you can override it in your subclass
+ pass
+
+ # TODO: these are module scope static function candidates
+ # (most of the class is really...)
+ def get_options(self, option_string):
+ start = option_string.find('<') + 1
+ end = option_string.rfind('>')
+ if (start > 0) and (end > 0) and (end > start + 1):
+ option_csv = option_string[start:end]
+ return option_csv.split(',')
+ else:
+ return []
+
+ def merge_default_interface(self, defaults, interfaces, ip_type):
+ if 'interface' not in defaults:
+ return
+ if not defaults['interface'] in interfaces:
+ return
+ ifinfo = interfaces[defaults['interface']]
+ # copy all the interface values across except addresses
+ for item in ifinfo:
+ if item != 'ipv4' and item != 'ipv6':
+ defaults[item] = ifinfo[item]
+
+ ipinfo = []
+ if 'address' in defaults:
+ ipinfo = [x for x in ifinfo[ip_type] if x['address'] == defaults['address']]
+
+ if len(ipinfo) == 0:
+ ipinfo = ifinfo[ip_type]
+
+ if len(ipinfo) > 0:
+ for item in ipinfo[0]:
+ defaults[item] = ipinfo[0][item]
diff --git a/lib/ansible/module_utils/facts/network/hpux.py b/lib/ansible/module_utils/facts/network/hpux.py
new file mode 100644
index 0000000..add57be
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/hpux.py
@@ -0,0 +1,82 @@
+# 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
+
+from ansible.module_utils.facts.network.base import Network, NetworkCollector
+
+
+class HPUXNetwork(Network):
+ """
+ HP-UX-specifig subclass of Network. Defines networking facts:
+ - default_interface
+ - interfaces (a list of interface names)
+ - interface_<name> dictionary of ipv4 address information.
+ """
+ platform = 'HP-UX'
+
+ def populate(self, collected_facts=None):
+ network_facts = {}
+ netstat_path = self.module.get_bin_path('netstat')
+
+ if netstat_path is None:
+ return network_facts
+
+ default_interfaces_facts = self.get_default_interfaces()
+ network_facts.update(default_interfaces_facts)
+
+ interfaces = self.get_interfaces_info()
+ network_facts['interfaces'] = interfaces.keys()
+ for iface in interfaces:
+ network_facts[iface] = interfaces[iface]
+
+ return network_facts
+
+ def get_default_interfaces(self):
+ default_interfaces = {}
+ rc, out, err = self.module.run_command("/usr/bin/netstat -nr")
+ lines = out.splitlines()
+ for line in lines:
+ words = line.split()
+ if len(words) > 1:
+ if words[0] == 'default':
+ default_interfaces['default_interface'] = words[4]
+ default_interfaces['default_gateway'] = words[1]
+
+ return default_interfaces
+
+ def get_interfaces_info(self):
+ interfaces = {}
+ rc, out, err = self.module.run_command("/usr/bin/netstat -niw")
+ lines = out.splitlines()
+ for line in lines:
+ words = line.split()
+ for i in range(len(words) - 1):
+ if words[i][:3] == 'lan':
+ device = words[i]
+ interfaces[device] = {'device': device}
+ address = words[i + 3]
+ interfaces[device]['ipv4'] = {'address': address}
+ network = words[i + 2]
+ interfaces[device]['ipv4'] = {'network': network,
+ 'interface': device,
+ 'address': address}
+ return interfaces
+
+
+class HPUXNetworkCollector(NetworkCollector):
+ _fact_class = HPUXNetwork
+ _platform = 'HP-UX'
diff --git a/lib/ansible/module_utils/facts/network/hurd.py b/lib/ansible/module_utils/facts/network/hurd.py
new file mode 100644
index 0000000..518df39
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/hurd.py
@@ -0,0 +1,87 @@
+# 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
+
+from ansible.module_utils.facts.network.base import Network, NetworkCollector
+
+
+class HurdPfinetNetwork(Network):
+ """
+ This is a GNU Hurd specific subclass of Network. It use fsysopts to
+ get the ip address and support only pfinet.
+ """
+ platform = 'GNU'
+ _socket_dir = '/servers/socket/'
+
+ def assign_network_facts(self, network_facts, fsysopts_path, socket_path):
+ rc, out, err = self.module.run_command([fsysopts_path, '-L', socket_path])
+ # FIXME: build up a interfaces datastructure, then assign into network_facts
+ network_facts['interfaces'] = []
+ for i in out.split():
+ if '=' in i and i.startswith('--'):
+ k, v = i.split('=', 1)
+ # remove '--'
+ k = k[2:]
+ if k == 'interface':
+ # remove /dev/ from /dev/eth0
+ v = v[5:]
+ network_facts['interfaces'].append(v)
+ network_facts[v] = {
+ 'active': True,
+ 'device': v,
+ 'ipv4': {},
+ 'ipv6': [],
+ }
+ current_if = v
+ elif k == 'address':
+ network_facts[current_if]['ipv4']['address'] = v
+ elif k == 'netmask':
+ network_facts[current_if]['ipv4']['netmask'] = v
+ elif k == 'address6':
+ address, prefix = v.split('/')
+ network_facts[current_if]['ipv6'].append({
+ 'address': address,
+ 'prefix': prefix,
+ })
+ return network_facts
+
+ def populate(self, collected_facts=None):
+ network_facts = {}
+
+ fsysopts_path = self.module.get_bin_path('fsysopts')
+ if fsysopts_path is None:
+ return network_facts
+
+ socket_path = None
+
+ for l in ('inet', 'inet6'):
+ link = os.path.join(self._socket_dir, l)
+ if os.path.exists(link):
+ socket_path = link
+ break
+
+ if socket_path is None:
+ return network_facts
+
+ return self.assign_network_facts(network_facts, fsysopts_path, socket_path)
+
+
+class HurdNetworkCollector(NetworkCollector):
+ _platform = 'GNU'
+ _fact_class = HurdPfinetNetwork
diff --git a/lib/ansible/module_utils/facts/network/iscsi.py b/lib/ansible/module_utils/facts/network/iscsi.py
new file mode 100644
index 0000000..2bb9383
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/iscsi.py
@@ -0,0 +1,115 @@
+# iSCSI initiator 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 sys
+import subprocess
+
+import ansible.module_utils.compat.typing as t
+
+from ansible.module_utils.common.process import get_bin_path
+from ansible.module_utils.facts.utils import get_file_content
+from ansible.module_utils.facts.network.base import NetworkCollector
+
+
+class IscsiInitiatorNetworkCollector(NetworkCollector):
+ name = 'iscsi'
+ _fact_ids = set() # type: t.Set[str]
+
+ def collect(self, module=None, collected_facts=None):
+ """
+ Example of contents of /etc/iscsi/initiatorname.iscsi:
+
+ ## DO NOT EDIT OR REMOVE THIS FILE!
+ ## If you remove this file, the iSCSI daemon will not start.
+ ## If you change the InitiatorName, existing access control lists
+ ## may reject this initiator. The InitiatorName must be unique
+ ## for each iSCSI initiator. Do NOT duplicate iSCSI InitiatorNames.
+ InitiatorName=iqn.1993-08.org.debian:01:44a42c8ddb8b
+
+ Example of output from the AIX lsattr command:
+
+ # lsattr -E -l iscsi0
+ disc_filename /etc/iscsi/targets Configuration file False
+ disc_policy file Discovery Policy True
+ initiator_name iqn.localhost.hostid.7f000002 iSCSI Initiator Name True
+ isns_srvnames auto iSNS Servers IP Addresses True
+ isns_srvports iSNS Servers Port Numbers True
+ max_targets 16 Maximum Targets Allowed True
+ num_cmd_elems 200 Maximum number of commands to queue to driver True
+
+ Example of output from the HP-UX iscsiutil command:
+
+ #iscsiutil -l
+ Initiator Name : iqn.1986-03.com.hp:mcel_VMhost3.1f355cf6-e2db-11e0-a999-b44c0aef5537
+ Initiator Alias :
+
+ Authentication Method : None
+ CHAP Method : CHAP_UNI
+ Initiator CHAP Name :
+ CHAP Secret :
+ NAS Hostname :
+ NAS Secret :
+ Radius Server Hostname :
+ Header Digest : None, CRC32C (default)
+ Data Digest : None, CRC32C (default)
+ SLP Scope list for iSLPD :
+ """
+
+ iscsi_facts = {}
+ iscsi_facts['iscsi_iqn'] = ""
+ if sys.platform.startswith('linux') or sys.platform.startswith('sunos'):
+ for line in get_file_content('/etc/iscsi/initiatorname.iscsi', '').splitlines():
+ if line.startswith('#') or line.startswith(';') or line.strip() == '':
+ continue
+ if line.startswith('InitiatorName='):
+ iscsi_facts['iscsi_iqn'] = line.split('=', 1)[1]
+ break
+ elif sys.platform.startswith('aix'):
+ try:
+ cmd = get_bin_path('lsattr')
+ except ValueError:
+ return iscsi_facts
+
+ cmd += " -E -l iscsi0"
+ rc, out, err = module.run_command(cmd)
+ if rc == 0 and out:
+ line = self.findstr(out, 'initiator_name')
+ iscsi_facts['iscsi_iqn'] = line.split()[1].rstrip()
+
+ elif sys.platform.startswith('hp-ux'):
+ # try to find it in the default PATH and opt_dirs
+ try:
+ cmd = get_bin_path('iscsiutil', opt_dirs=['/opt/iscsi/bin'])
+ except ValueError:
+ return iscsi_facts
+
+ cmd += " -l"
+ rc, out, err = module.run_command(cmd)
+ if out:
+ line = self.findstr(out, 'Initiator Name')
+ iscsi_facts['iscsi_iqn'] = line.split(":", 1)[1].rstrip()
+
+ return iscsi_facts
+
+ def findstr(self, text, match):
+ for line in text.splitlines():
+ if match in line:
+ found = line
+ return found
diff --git a/lib/ansible/module_utils/facts/network/linux.py b/lib/ansible/module_utils/facts/network/linux.py
new file mode 100644
index 0000000..b7ae976
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/linux.py
@@ -0,0 +1,327 @@
+# 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 os
+import re
+import socket
+import struct
+
+from ansible.module_utils.facts.network.base import Network, NetworkCollector
+
+from ansible.module_utils.facts.utils import get_file_content
+
+
+class LinuxNetwork(Network):
+ """
+ This is a Linux-specific subclass of Network. It defines
+ - interfaces (a list of interface names)
+ - interface_<name> dictionary of ipv4, ipv6, and mac address information.
+ - all_ipv4_addresses and all_ipv6_addresses: lists of all configured addresses.
+ - ipv4_address and ipv6_address: the first non-local address for each family.
+ """
+ platform = 'Linux'
+ INTERFACE_TYPE = {
+ '1': 'ether',
+ '32': 'infiniband',
+ '512': 'ppp',
+ '772': 'loopback',
+ '65534': 'tunnel',
+ }
+
+ def populate(self, collected_facts=None):
+ network_facts = {}
+ ip_path = self.module.get_bin_path('ip')
+ if ip_path is None:
+ return network_facts
+ default_ipv4, default_ipv6 = self.get_default_interfaces(ip_path,
+ collected_facts=collected_facts)
+ interfaces, ips = self.get_interfaces_info(ip_path, default_ipv4, default_ipv6)
+ network_facts['interfaces'] = interfaces.keys()
+ for iface in interfaces:
+ network_facts[iface] = interfaces[iface]
+ network_facts['default_ipv4'] = default_ipv4
+ network_facts['default_ipv6'] = default_ipv6
+ network_facts['all_ipv4_addresses'] = ips['all_ipv4_addresses']
+ network_facts['all_ipv6_addresses'] = ips['all_ipv6_addresses']
+ return network_facts
+
+ def get_default_interfaces(self, ip_path, collected_facts=None):
+ collected_facts = collected_facts or {}
+ # Use the commands:
+ # ip -4 route get 8.8.8.8 -> Google public DNS
+ # ip -6 route get 2404:6800:400a:800::1012 -> ipv6.google.com
+ # to find out the default outgoing interface, address, and gateway
+ command = dict(
+ v4=[ip_path, '-4', 'route', 'get', '8.8.8.8'],
+ v6=[ip_path, '-6', 'route', 'get', '2404:6800:400a:800::1012']
+ )
+ interface = dict(v4={}, v6={})
+
+ for v in 'v4', 'v6':
+ if (v == 'v6' and collected_facts.get('ansible_os_family') == 'RedHat' and
+ collected_facts.get('ansible_distribution_version', '').startswith('4.')):
+ continue
+ if v == 'v6' and not socket.has_ipv6:
+ continue
+ rc, out, err = self.module.run_command(command[v], errors='surrogate_then_replace')
+ if not out:
+ # v6 routing may result in
+ # RTNETLINK answers: Invalid argument
+ continue
+ words = out.splitlines()[0].split()
+ # A valid output starts with the queried address on the first line
+ if len(words) > 0 and words[0] == command[v][-1]:
+ for i in range(len(words) - 1):
+ if words[i] == 'dev':
+ interface[v]['interface'] = words[i + 1]
+ elif words[i] == 'src':
+ interface[v]['address'] = words[i + 1]
+ elif words[i] == 'via' and words[i + 1] != command[v][-1]:
+ interface[v]['gateway'] = words[i + 1]
+ return interface['v4'], interface['v6']
+
+ def get_interfaces_info(self, ip_path, default_ipv4, default_ipv6):
+ interfaces = {}
+ ips = dict(
+ all_ipv4_addresses=[],
+ all_ipv6_addresses=[],
+ )
+
+ # FIXME: maybe split into smaller methods?
+ # FIXME: this is pretty much a constructor
+
+ for path in glob.glob('/sys/class/net/*'):
+ if not os.path.isdir(path):
+ continue
+ device = os.path.basename(path)
+ interfaces[device] = {'device': device}
+ if os.path.exists(os.path.join(path, 'address')):
+ macaddress = get_file_content(os.path.join(path, 'address'), default='')
+ if macaddress and macaddress != '00:00:00:00:00:00':
+ interfaces[device]['macaddress'] = macaddress
+ if os.path.exists(os.path.join(path, 'mtu')):
+ interfaces[device]['mtu'] = int(get_file_content(os.path.join(path, 'mtu')))
+ if os.path.exists(os.path.join(path, 'operstate')):
+ interfaces[device]['active'] = get_file_content(os.path.join(path, 'operstate')) != 'down'
+ if os.path.exists(os.path.join(path, 'device', 'driver', 'module')):
+ interfaces[device]['module'] = os.path.basename(os.path.realpath(os.path.join(path, 'device', 'driver', 'module')))
+ if os.path.exists(os.path.join(path, 'type')):
+ _type = get_file_content(os.path.join(path, 'type'))
+ interfaces[device]['type'] = self.INTERFACE_TYPE.get(_type, 'unknown')
+ if os.path.exists(os.path.join(path, 'bridge')):
+ interfaces[device]['type'] = 'bridge'
+ interfaces[device]['interfaces'] = [os.path.basename(b) for b in glob.glob(os.path.join(path, 'brif', '*'))]
+ if os.path.exists(os.path.join(path, 'bridge', 'bridge_id')):
+ interfaces[device]['id'] = get_file_content(os.path.join(path, 'bridge', 'bridge_id'), default='')
+ if os.path.exists(os.path.join(path, 'bridge', 'stp_state')):
+ interfaces[device]['stp'] = get_file_content(os.path.join(path, 'bridge', 'stp_state')) == '1'
+ if os.path.exists(os.path.join(path, 'bonding')):
+ interfaces[device]['type'] = 'bonding'
+ interfaces[device]['slaves'] = get_file_content(os.path.join(path, 'bonding', 'slaves'), default='').split()
+ interfaces[device]['mode'] = get_file_content(os.path.join(path, 'bonding', 'mode'), default='').split()[0]
+ interfaces[device]['miimon'] = get_file_content(os.path.join(path, 'bonding', 'miimon'), default='').split()[0]
+ interfaces[device]['lacp_rate'] = get_file_content(os.path.join(path, 'bonding', 'lacp_rate'), default='').split()[0]
+ primary = get_file_content(os.path.join(path, 'bonding', 'primary'))
+ if primary:
+ interfaces[device]['primary'] = primary
+ path = os.path.join(path, 'bonding', 'all_slaves_active')
+ if os.path.exists(path):
+ interfaces[device]['all_slaves_active'] = get_file_content(path) == '1'
+ if os.path.exists(os.path.join(path, 'bonding_slave')):
+ interfaces[device]['perm_macaddress'] = get_file_content(os.path.join(path, 'bonding_slave', 'perm_hwaddr'), default='')
+ if os.path.exists(os.path.join(path, 'device')):
+ interfaces[device]['pciid'] = os.path.basename(os.readlink(os.path.join(path, 'device')))
+ if os.path.exists(os.path.join(path, 'speed')):
+ speed = get_file_content(os.path.join(path, 'speed'))
+ if speed is not None:
+ interfaces[device]['speed'] = int(speed)
+
+ # Check whether an interface is in promiscuous mode
+ if os.path.exists(os.path.join(path, 'flags')):
+ promisc_mode = False
+ # The second byte indicates whether the interface is in promiscuous mode.
+ # 1 = promisc
+ # 0 = no promisc
+ data = int(get_file_content(os.path.join(path, 'flags')), 16)
+ promisc_mode = (data & 0x0100 > 0)
+ interfaces[device]['promisc'] = promisc_mode
+
+ # TODO: determine if this needs to be in a nested scope/closure
+ def parse_ip_output(output, secondary=False):
+ for line in output.splitlines():
+ if not line:
+ continue
+ words = line.split()
+ broadcast = ''
+ if words[0] == 'inet':
+ if '/' in words[1]:
+ address, netmask_length = words[1].split('/')
+ if len(words) > 3:
+ if words[2] == 'brd':
+ broadcast = words[3]
+ else:
+ # pointopoint interfaces do not have a prefix
+ address = words[1]
+ netmask_length = "32"
+ address_bin = struct.unpack('!L', socket.inet_aton(address))[0]
+ netmask_bin = (1 << 32) - (1 << 32 >> int(netmask_length))
+ netmask = socket.inet_ntoa(struct.pack('!L', netmask_bin))
+ network = socket.inet_ntoa(struct.pack('!L', address_bin & netmask_bin))
+ iface = words[-1]
+ # NOTE: device is ref to outside scope
+ # NOTE: interfaces is also ref to outside scope
+ if iface != device:
+ interfaces[iface] = {}
+ if not secondary and "ipv4" not in interfaces[iface]:
+ interfaces[iface]['ipv4'] = {'address': address,
+ 'broadcast': broadcast,
+ 'netmask': netmask,
+ 'network': network,
+ 'prefix': netmask_length,
+ }
+ else:
+ if "ipv4_secondaries" not in interfaces[iface]:
+ interfaces[iface]["ipv4_secondaries"] = []
+ interfaces[iface]["ipv4_secondaries"].append({
+ 'address': address,
+ 'broadcast': broadcast,
+ 'netmask': netmask,
+ 'network': network,
+ 'prefix': netmask_length,
+ })
+
+ # add this secondary IP to the main device
+ if secondary:
+ if "ipv4_secondaries" not in interfaces[device]:
+ interfaces[device]["ipv4_secondaries"] = []
+ if device != iface:
+ interfaces[device]["ipv4_secondaries"].append({
+ 'address': address,
+ 'broadcast': broadcast,
+ 'netmask': netmask,
+ 'network': network,
+ 'prefix': netmask_length,
+ })
+
+ # NOTE: default_ipv4 is ref to outside scope
+ # If this is the default address, update default_ipv4
+ if 'address' in default_ipv4 and default_ipv4['address'] == address:
+ default_ipv4['broadcast'] = broadcast
+ default_ipv4['netmask'] = netmask
+ default_ipv4['network'] = network
+ default_ipv4['prefix'] = netmask_length
+ # NOTE: macaddress is ref from outside scope
+ default_ipv4['macaddress'] = macaddress
+ default_ipv4['mtu'] = interfaces[device]['mtu']
+ default_ipv4['type'] = interfaces[device].get("type", "unknown")
+ default_ipv4['alias'] = words[-1]
+ if not address.startswith('127.'):
+ ips['all_ipv4_addresses'].append(address)
+ elif words[0] == 'inet6':
+ if 'peer' == words[2]:
+ address = words[1]
+ _, prefix = words[3].split('/')
+ scope = words[5]
+ else:
+ address, prefix = words[1].split('/')
+ scope = words[3]
+ if 'ipv6' not in interfaces[device]:
+ interfaces[device]['ipv6'] = []
+ interfaces[device]['ipv6'].append({
+ 'address': address,
+ 'prefix': prefix,
+ 'scope': scope
+ })
+ # If this is the default address, update default_ipv6
+ if 'address' in default_ipv6 and default_ipv6['address'] == address:
+ default_ipv6['prefix'] = prefix
+ default_ipv6['scope'] = scope
+ default_ipv6['macaddress'] = macaddress
+ default_ipv6['mtu'] = interfaces[device]['mtu']
+ default_ipv6['type'] = interfaces[device].get("type", "unknown")
+ if not address == '::1':
+ ips['all_ipv6_addresses'].append(address)
+
+ ip_path = self.module.get_bin_path("ip")
+
+ args = [ip_path, 'addr', 'show', 'primary', 'dev', device]
+ rc, primary_data, stderr = self.module.run_command(args, errors='surrogate_then_replace')
+ if rc == 0:
+ parse_ip_output(primary_data)
+ else:
+ # possibly busybox, fallback to running without the "primary" arg
+ # https://github.com/ansible/ansible/issues/50871
+ args = [ip_path, 'addr', 'show', 'dev', device]
+ rc, data, stderr = self.module.run_command(args, errors='surrogate_then_replace')
+ if rc == 0:
+ parse_ip_output(data)
+
+ args = [ip_path, 'addr', 'show', 'secondary', 'dev', device]
+ rc, secondary_data, stderr = self.module.run_command(args, errors='surrogate_then_replace')
+ if rc == 0:
+ parse_ip_output(secondary_data, secondary=True)
+
+ interfaces[device].update(self.get_ethtool_data(device))
+
+ # replace : by _ in interface name since they are hard to use in template
+ new_interfaces = {}
+ # i is a dict key (string) not an index int
+ for i in interfaces:
+ if ':' in i:
+ new_interfaces[i.replace(':', '_')] = interfaces[i]
+ else:
+ new_interfaces[i] = interfaces[i]
+ return new_interfaces, ips
+
+ def get_ethtool_data(self, device):
+
+ data = {}
+ ethtool_path = self.module.get_bin_path("ethtool")
+ # FIXME: exit early on falsey ethtool_path and un-indent
+ if ethtool_path:
+ args = [ethtool_path, '-k', device]
+ rc, stdout, stderr = self.module.run_command(args, errors='surrogate_then_replace')
+ # FIXME: exit early on falsey if we can
+ if rc == 0:
+ features = {}
+ for line in stdout.strip().splitlines():
+ if not line or line.endswith(":"):
+ continue
+ key, value = line.split(": ")
+ if not value:
+ continue
+ features[key.strip().replace('-', '_')] = value.strip()
+ data['features'] = features
+
+ args = [ethtool_path, '-T', device]
+ rc, stdout, stderr = self.module.run_command(args, errors='surrogate_then_replace')
+ if rc == 0:
+ data['timestamping'] = [m.lower() for m in re.findall(r'SOF_TIMESTAMPING_(\w+)', stdout)]
+ data['hw_timestamp_filters'] = [m.lower() for m in re.findall(r'HWTSTAMP_FILTER_(\w+)', stdout)]
+ m = re.search(r'PTP Hardware Clock: (\d+)', stdout)
+ if m:
+ data['phc_index'] = int(m.groups()[0])
+
+ return data
+
+
+class LinuxNetworkCollector(NetworkCollector):
+ _platform = 'Linux'
+ _fact_class = LinuxNetwork
+ required_facts = set(['distribution', 'platform'])
diff --git a/lib/ansible/module_utils/facts/network/netbsd.py b/lib/ansible/module_utils/facts/network/netbsd.py
new file mode 100644
index 0000000..de8ceff
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/netbsd.py
@@ -0,0 +1,48 @@
+# 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
+
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
+
+
+class NetBSDNetwork(GenericBsdIfconfigNetwork):
+ """
+ This is the NetBSD Network Class.
+ It uses the GenericBsdIfconfigNetwork
+ """
+ platform = 'NetBSD'
+
+ def parse_media_line(self, words, current_if, ips):
+ # example of line:
+ # $ ifconfig
+ # ne0: flags=8863<UP,BROADCAST,NOTRAILERS,RUNNING,SIMPLEX,MULTICAST> mtu 1500
+ # ec_capabilities=1<VLAN_MTU>
+ # ec_enabled=0
+ # address: 00:20:91:45:00:78
+ # media: Ethernet 10baseT full-duplex
+ # inet 192.168.156.29 netmask 0xffffff00 broadcast 192.168.156.255
+ current_if['media'] = words[1]
+ if len(words) > 2:
+ current_if['media_type'] = words[2]
+ if len(words) > 3:
+ current_if['media_options'] = words[3].split(',')
+
+
+class NetBSDNetworkCollector(NetworkCollector):
+ _fact_class = NetBSDNetwork
+ _platform = 'NetBSD'
diff --git a/lib/ansible/module_utils/facts/network/nvme.py b/lib/ansible/module_utils/facts/network/nvme.py
new file mode 100644
index 0000000..febd0ab
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/nvme.py
@@ -0,0 +1,57 @@
+# NVMe initiator 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 sys
+import subprocess
+
+import ansible.module_utils.compat.typing as t
+
+from ansible.module_utils.facts.utils import get_file_content
+from ansible.module_utils.facts.network.base import NetworkCollector
+
+
+class NvmeInitiatorNetworkCollector(NetworkCollector):
+ name = 'nvme'
+ _fact_ids = set() # type: t.Set[str]
+
+ def collect(self, module=None, collected_facts=None):
+ """
+ Currently NVMe is only supported in some Linux distributions.
+ If NVMe is configured on the host then a file will have been created
+ during the NVMe driver installation. This file holds the unique NQN
+ of the host.
+
+ Example of contents of /etc/nvme/hostnqn:
+
+ # cat /etc/nvme/hostnqn
+ nqn.2014-08.org.nvmexpress:fc_lif:uuid:2cd61a74-17f9-4c22-b350-3020020c458d
+
+ """
+
+ nvme_facts = {}
+ nvme_facts['hostnqn'] = ""
+ if sys.platform.startswith('linux'):
+ for line in get_file_content('/etc/nvme/hostnqn', '').splitlines():
+ if line.startswith('#') or line.startswith(';') or line.strip() == '':
+ continue
+ if line.startswith('nqn.'):
+ nvme_facts['hostnqn'] = line
+ break
+ return nvme_facts
diff --git a/lib/ansible/module_utils/facts/network/openbsd.py b/lib/ansible/module_utils/facts/network/openbsd.py
new file mode 100644
index 0000000..9e11d82
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/openbsd.py
@@ -0,0 +1,42 @@
+# 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
+
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
+
+
+class OpenBSDNetwork(GenericBsdIfconfigNetwork):
+ """
+ This is the OpenBSD Network Class.
+ It uses the GenericBsdIfconfigNetwork.
+ """
+ platform = 'OpenBSD'
+
+ # OpenBSD 'ifconfig -a' does not have information about aliases
+ def get_interfaces_info(self, ifconfig_path, ifconfig_options='-aA'):
+ return super(OpenBSDNetwork, self).get_interfaces_info(ifconfig_path, ifconfig_options)
+
+ # Return macaddress instead of lladdr
+ def parse_lladdr_line(self, words, current_if, ips):
+ current_if['macaddress'] = words[1]
+ current_if['type'] = 'ether'
+
+
+class OpenBSDNetworkCollector(NetworkCollector):
+ _fact_class = OpenBSDNetwork
+ _platform = 'OpenBSD'
diff --git a/lib/ansible/module_utils/facts/network/sunos.py b/lib/ansible/module_utils/facts/network/sunos.py
new file mode 100644
index 0000000..adba14c
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/sunos.py
@@ -0,0 +1,116 @@
+# 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
+
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
+
+
+class SunOSNetwork(GenericBsdIfconfigNetwork):
+ """
+ This is the SunOS Network Class.
+ It uses the GenericBsdIfconfigNetwork.
+
+ Solaris can have different FLAGS and MTU for IPv4 and IPv6 on the same interface
+ so these facts have been moved inside the 'ipv4' and 'ipv6' lists.
+ """
+ platform = 'SunOS'
+
+ # Solaris 'ifconfig -a' will print interfaces twice, once for IPv4 and again for IPv6.
+ # MTU and FLAGS also may differ between IPv4 and IPv6 on the same interface.
+ # 'parse_interface_line()' checks for previously seen interfaces before defining
+ # 'current_if' so that IPv6 facts don't clobber IPv4 facts (or vice versa).
+ def get_interfaces_info(self, ifconfig_path):
+ interfaces = {}
+ current_if = {}
+ ips = dict(
+ all_ipv4_addresses=[],
+ all_ipv6_addresses=[],
+ )
+ rc, out, err = self.module.run_command([ifconfig_path, '-a'])
+
+ for line in out.splitlines():
+
+ if line:
+ words = line.split()
+
+ if re.match(r'^\S', line) and len(words) > 3:
+ current_if = self.parse_interface_line(words, current_if, interfaces)
+ interfaces[current_if['device']] = current_if
+ elif words[0].startswith('options='):
+ self.parse_options_line(words, current_if, ips)
+ elif words[0] == 'nd6':
+ self.parse_nd6_line(words, current_if, ips)
+ elif words[0] == 'ether':
+ self.parse_ether_line(words, current_if, ips)
+ elif words[0] == 'media:':
+ self.parse_media_line(words, current_if, ips)
+ elif words[0] == 'status:':
+ self.parse_status_line(words, current_if, ips)
+ elif words[0] == 'lladdr':
+ self.parse_lladdr_line(words, current_if, ips)
+ elif words[0] == 'inet':
+ self.parse_inet_line(words, current_if, ips)
+ elif words[0] == 'inet6':
+ self.parse_inet6_line(words, current_if, ips)
+ else:
+ self.parse_unknown_line(words, current_if, ips)
+
+ # 'parse_interface_line' and 'parse_inet*_line' leave two dicts in the
+ # ipv4/ipv6 lists which is ugly and hard to read.
+ # This quick hack merges the dictionaries. Purely cosmetic.
+ for iface in interfaces:
+ for v in 'ipv4', 'ipv6':
+ combined_facts = {}
+ for facts in interfaces[iface][v]:
+ combined_facts.update(facts)
+ if len(combined_facts.keys()) > 0:
+ interfaces[iface][v] = [combined_facts]
+
+ return interfaces, ips
+
+ def parse_interface_line(self, words, current_if, interfaces):
+ device = words[0][0:-1]
+ if device not in interfaces:
+ current_if = {'device': device, 'ipv4': [], 'ipv6': [], 'type': 'unknown'}
+ else:
+ current_if = interfaces[device]
+ flags = self.get_options(words[1])
+ v = 'ipv4'
+ if 'IPv6' in flags:
+ v = 'ipv6'
+ if 'LOOPBACK' in flags:
+ current_if['type'] = 'loopback'
+ current_if[v].append({'flags': flags, 'mtu': words[3]})
+ current_if['macaddress'] = 'unknown' # will be overwritten later
+ return current_if
+
+ # Solaris displays single digit octets in MAC addresses e.g. 0:1:2:d:e:f
+ # Add leading zero to each octet where needed.
+ def parse_ether_line(self, words, current_if, ips):
+ macaddress = ''
+ for octet in words[1].split(':'):
+ octet = ('0' + octet)[-2:None]
+ macaddress += (octet + ':')
+ current_if['macaddress'] = macaddress[0:-1]
+
+
+class SunOSNetworkCollector(NetworkCollector):
+ _fact_class = SunOSNetwork
+ _platform = 'SunOS'
diff --git a/lib/ansible/module_utils/facts/other/__init__.py b/lib/ansible/module_utils/facts/other/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/ansible/module_utils/facts/other/__init__.py
diff --git a/lib/ansible/module_utils/facts/other/facter.py b/lib/ansible/module_utils/facts/other/facter.py
new file mode 100644
index 0000000..3f83999
--- /dev/null
+++ b/lib/ansible/module_utils/facts/other/facter.py
@@ -0,0 +1,87 @@
+# 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 json
+
+import ansible.module_utils.compat.typing as t
+
+from ansible.module_utils.facts.namespace import PrefixFactNamespace
+
+from ansible.module_utils.facts.collector import BaseFactCollector
+
+
+class FacterFactCollector(BaseFactCollector):
+ name = 'facter'
+ _fact_ids = set(['facter']) # type: t.Set[str]
+
+ def __init__(self, collectors=None, namespace=None):
+ namespace = PrefixFactNamespace(namespace_name='facter',
+ prefix='facter_')
+ super(FacterFactCollector, self).__init__(collectors=collectors,
+ namespace=namespace)
+
+ def find_facter(self, module):
+ facter_path = module.get_bin_path('facter', opt_dirs=['/opt/puppetlabs/bin'])
+ cfacter_path = module.get_bin_path('cfacter', opt_dirs=['/opt/puppetlabs/bin'])
+
+ # Prefer to use cfacter if available
+ if cfacter_path is not None:
+ facter_path = cfacter_path
+
+ return facter_path
+
+ def run_facter(self, module, facter_path):
+ # if facter is installed, and we can use --json because
+ # ruby-json is ALSO installed, include facter data in the JSON
+ rc, out, err = module.run_command(facter_path + " --puppet --json")
+ return rc, out, err
+
+ def get_facter_output(self, module):
+ facter_path = self.find_facter(module)
+ if not facter_path:
+ return None
+
+ rc, out, err = self.run_facter(module, facter_path)
+
+ if rc != 0:
+ return None
+
+ return out
+
+ def collect(self, module=None, collected_facts=None):
+ # Note that this mirrors previous facter behavior, where there isnt
+ # a 'ansible_facter' key in the main fact dict, but instead, 'facter_whatever'
+ # items are added to the main dict.
+ facter_dict = {}
+
+ if not module:
+ return facter_dict
+
+ facter_output = self.get_facter_output(module)
+
+ # TODO: if we fail, should we add a empty facter key or nothing?
+ if facter_output is None:
+ return facter_dict
+
+ try:
+ facter_dict = json.loads(facter_output)
+ except Exception:
+ # FIXME: maybe raise a FactCollectorError with some info attrs?
+ pass
+
+ return facter_dict
diff --git a/lib/ansible/module_utils/facts/other/ohai.py b/lib/ansible/module_utils/facts/other/ohai.py
new file mode 100644
index 0000000..90c5539
--- /dev/null
+++ b/lib/ansible/module_utils/facts/other/ohai.py
@@ -0,0 +1,74 @@
+# 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 json
+
+import ansible.module_utils.compat.typing as t
+
+from ansible.module_utils.facts.namespace import PrefixFactNamespace
+
+from ansible.module_utils.facts.collector import BaseFactCollector
+
+
+class OhaiFactCollector(BaseFactCollector):
+ '''This is a subclass of Facts for including information gathered from Ohai.'''
+ name = 'ohai'
+ _fact_ids = set() # type: t.Set[str]
+
+ def __init__(self, collectors=None, namespace=None):
+ namespace = PrefixFactNamespace(namespace_name='ohai',
+ prefix='ohai_')
+ super(OhaiFactCollector, self).__init__(collectors=collectors,
+ namespace=namespace)
+
+ def find_ohai(self, module):
+ ohai_path = module.get_bin_path('ohai')
+ return ohai_path
+
+ def run_ohai(self, module, ohai_path,):
+ rc, out, err = module.run_command(ohai_path)
+ return rc, out, err
+
+ def get_ohai_output(self, module):
+ ohai_path = self.find_ohai(module)
+ if not ohai_path:
+ return None
+
+ rc, out, err = self.run_ohai(module, ohai_path)
+ if rc != 0:
+ return None
+
+ return out
+
+ def collect(self, module=None, collected_facts=None):
+ ohai_facts = {}
+ if not module:
+ return ohai_facts
+
+ ohai_output = self.get_ohai_output(module)
+
+ if ohai_output is None:
+ return ohai_facts
+
+ try:
+ ohai_facts = json.loads(ohai_output)
+ except Exception:
+ # FIXME: useful error, logging, something...
+ pass
+
+ return ohai_facts
diff --git a/lib/ansible/module_utils/facts/packages.py b/lib/ansible/module_utils/facts/packages.py
new file mode 100644
index 0000000..53f74a1
--- /dev/null
+++ b/lib/ansible/module_utils/facts/packages.py
@@ -0,0 +1,86 @@
+# (c) 2018, Ansible Project
+# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from abc import ABCMeta, abstractmethod
+
+from ansible.module_utils.six import with_metaclass
+from ansible.module_utils.common.process import get_bin_path
+from ansible.module_utils.common._utils import get_all_subclasses
+
+
+def get_all_pkg_managers():
+
+ return {obj.__name__.lower(): obj for obj in get_all_subclasses(PkgMgr) if obj not in (CLIMgr, LibMgr)}
+
+
+class PkgMgr(with_metaclass(ABCMeta, object)): # type: ignore[misc]
+
+ @abstractmethod
+ def is_available(self):
+ # This method is supposed to return True/False if the package manager is currently installed/usable
+ # It can also 'prep' the required systems in the process of detecting availability
+ pass
+
+ @abstractmethod
+ def list_installed(self):
+ # This method should return a list of installed packages, each list item will be passed to get_package_details
+ pass
+
+ @abstractmethod
+ def get_package_details(self, package):
+ # This takes a 'package' item and returns a dictionary with the package information, name and version are minimal requirements
+ pass
+
+ def get_packages(self):
+ # Take all of the above and return a dictionary of lists of dictionaries (package = list of installed versions)
+
+ installed_packages = {}
+ for package in self.list_installed():
+ package_details = self.get_package_details(package)
+ if 'source' not in package_details:
+ package_details['source'] = self.__class__.__name__.lower()
+ name = package_details['name']
+ if name not in installed_packages:
+ installed_packages[name] = [package_details]
+ else:
+ installed_packages[name].append(package_details)
+ return installed_packages
+
+
+class LibMgr(PkgMgr):
+
+ LIB = None # type: str | None
+
+ def __init__(self):
+
+ self._lib = None
+ super(LibMgr, self).__init__()
+
+ def is_available(self):
+ found = False
+ try:
+ self._lib = __import__(self.LIB)
+ found = True
+ except ImportError:
+ pass
+ return found
+
+
+class CLIMgr(PkgMgr):
+
+ CLI = None # type: str | None
+
+ def __init__(self):
+
+ self._cli = None
+ super(CLIMgr, self).__init__()
+
+ def is_available(self):
+ try:
+ self._cli = get_bin_path(self.CLI)
+ except ValueError:
+ return False
+ return True
diff --git a/lib/ansible/module_utils/facts/sysctl.py b/lib/ansible/module_utils/facts/sysctl.py
new file mode 100644
index 0000000..2c55d77
--- /dev/null
+++ b/lib/ansible/module_utils/facts/sysctl.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 re
+
+from ansible.module_utils._text import to_text
+
+
+def get_sysctl(module, prefixes):
+ sysctl_cmd = module.get_bin_path('sysctl')
+ cmd = [sysctl_cmd]
+ cmd.extend(prefixes)
+
+ sysctl = dict()
+
+ try:
+ rc, out, err = module.run_command(cmd)
+ except (IOError, OSError) as e:
+ module.warn('Unable to read sysctl: %s' % to_text(e))
+ rc = 1
+
+ if rc == 0:
+ key = ''
+ value = ''
+ for line in out.splitlines():
+ if not line.strip():
+ continue
+
+ if line.startswith(' '):
+ # handle multiline values, they will not have a starting key
+ # Add the newline back in so people can split on it to parse
+ # lines if they need to.
+ value += '\n' + line
+ continue
+
+ if key:
+ sysctl[key] = value.strip()
+
+ try:
+ (key, value) = re.split(r'\s?=\s?|: ', line, maxsplit=1)
+ except Exception as e:
+ module.warn('Unable to split sysctl line (%s): %s' % (to_text(line), to_text(e)))
+
+ if key:
+ sysctl[key] = value.strip()
+
+ return sysctl
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
diff --git a/lib/ansible/module_utils/facts/timeout.py b/lib/ansible/module_utils/facts/timeout.py
new file mode 100644
index 0000000..ebb71cc
--- /dev/null
+++ b/lib/ansible/module_utils/facts/timeout.py
@@ -0,0 +1,70 @@
+# 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 multiprocessing
+import multiprocessing.pool as mp
+
+# timeout function to make sure some fact gathering
+# steps do not exceed a time limit
+
+GATHER_TIMEOUT = None
+DEFAULT_GATHER_TIMEOUT = 10
+
+
+class TimeoutError(Exception):
+ pass
+
+
+def timeout(seconds=None, error_message="Timer expired"):
+ """
+ Timeout decorator to expire after a set number of seconds. This raises an
+ ansible.module_utils.facts.TimeoutError if the timeout is hit before the
+ function completes.
+ """
+ def decorator(func):
+ def wrapper(*args, **kwargs):
+ timeout_value = seconds
+ if timeout_value is None:
+ timeout_value = globals().get('GATHER_TIMEOUT') or DEFAULT_GATHER_TIMEOUT
+
+ pool = mp.ThreadPool(processes=1)
+ res = pool.apply_async(func, args, kwargs)
+ pool.close()
+ try:
+ return res.get(timeout_value)
+ except multiprocessing.TimeoutError:
+ # This is an ansible.module_utils.common.facts.timeout.TimeoutError
+ raise TimeoutError('Timer expired after %s seconds' % timeout_value)
+ finally:
+ pool.terminate()
+
+ return wrapper
+
+ # If we were called as @timeout, then the first parameter will be the
+ # function we are to wrap instead of the number of seconds. Detect this
+ # and correct it by setting seconds to our default value and return the
+ # inner decorator function manually wrapped around the function
+ if callable(seconds):
+ func = seconds
+ seconds = None
+ return decorator(func)
+
+ # If we were called as @timeout([...]) then python itself will take
+ # care of wrapping the inner decorator around the function
+
+ return decorator
diff --git a/lib/ansible/module_utils/facts/utils.py b/lib/ansible/module_utils/facts/utils.py
new file mode 100644
index 0000000..a6027ab
--- /dev/null
+++ b/lib/ansible/module_utils/facts/utils.py
@@ -0,0 +1,102 @@
+# 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 fcntl
+import os
+
+
+def get_file_content(path, default=None, strip=True):
+ '''
+ Return the contents of a given file path
+
+ :args path: path to file to return contents from
+ :args default: value to return if we could not read file
+ :args strip: controls if we strip whitespace from the result or not
+
+ :returns: String with file contents (optionally stripped) or 'default' value
+ '''
+ data = default
+ if os.path.exists(path) and os.access(path, os.R_OK):
+ datafile = None
+ try:
+ datafile = open(path)
+ try:
+ # try to not enter kernel 'block' mode, which prevents timeouts
+ fd = datafile.fileno()
+ flag = fcntl.fcntl(fd, fcntl.F_GETFL)
+ fcntl.fcntl(fd, fcntl.F_SETFL, flag | os.O_NONBLOCK)
+ except Exception:
+ pass # not required to operate, but would have been nice!
+
+ # actually read the data
+ data = datafile.read()
+
+ if strip:
+ data = data.strip()
+
+ if len(data) == 0:
+ data = default
+
+ except Exception:
+ # ignore errors as some jails/containers might have readable permissions but not allow reads
+ pass
+ finally:
+ if datafile is not None:
+ datafile.close()
+
+ return data
+
+
+def get_file_lines(path, strip=True, line_sep=None):
+ '''get list of lines from file'''
+ data = get_file_content(path, strip=strip)
+ if data:
+ if line_sep is None:
+ ret = data.splitlines()
+ else:
+ if len(line_sep) == 1:
+ ret = data.rstrip(line_sep).split(line_sep)
+ else:
+ ret = data.split(line_sep)
+ else:
+ ret = []
+ return ret
+
+
+def get_mount_size(mountpoint):
+ mount_size = {}
+
+ try:
+ statvfs_result = os.statvfs(mountpoint)
+ mount_size['size_total'] = statvfs_result.f_frsize * statvfs_result.f_blocks
+ mount_size['size_available'] = statvfs_result.f_frsize * (statvfs_result.f_bavail)
+
+ # Block total/available/used
+ mount_size['block_size'] = statvfs_result.f_bsize
+ mount_size['block_total'] = statvfs_result.f_blocks
+ mount_size['block_available'] = statvfs_result.f_bavail
+ mount_size['block_used'] = mount_size['block_total'] - mount_size['block_available']
+
+ # Inode total/available/used
+ mount_size['inode_total'] = statvfs_result.f_files
+ mount_size['inode_available'] = statvfs_result.f_favail
+ mount_size['inode_used'] = mount_size['inode_total'] - mount_size['inode_available']
+ except OSError:
+ pass
+
+ return mount_size
diff --git a/lib/ansible/module_utils/facts/virtual/__init__.py b/lib/ansible/module_utils/facts/virtual/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/ansible/module_utils/facts/virtual/__init__.py
diff --git a/lib/ansible/module_utils/facts/virtual/base.py b/lib/ansible/module_utils/facts/virtual/base.py
new file mode 100644
index 0000000..67b59a5
--- /dev/null
+++ b/lib/ansible/module_utils/facts/virtual/base.py
@@ -0,0 +1,80 @@
+# base classes for virtualization facts
+# -*- coding: utf-8 -*-
+#
+# 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
+
+
+class Virtual:
+ """
+ This is a generic Virtual subclass of Facts. This should be further
+ subclassed to implement per platform. If you subclass this,
+ you should define:
+ - virtualization_type
+ - virtualization_role
+ - container (e.g. solaris zones, freebsd jails, linux containers)
+
+ All subclasses MUST define platform.
+ """
+ platform = 'Generic'
+
+ # FIXME: remove load_on_init if we can
+ def __init__(self, module, load_on_init=False):
+ self.module = module
+
+ # FIXME: just here for existing tests cases till they are updated
+ def populate(self, collected_facts=None):
+ virtual_facts = self.get_virtual_facts()
+
+ return virtual_facts
+
+ def get_virtual_facts(self):
+ virtual_facts = {
+ 'virtualization_type': '',
+ 'virtualization_role': '',
+ 'virtualization_tech_guest': set(),
+ 'virtualization_tech_host': set(),
+ }
+ return virtual_facts
+
+
+class VirtualCollector(BaseFactCollector):
+ name = 'virtual'
+ _fact_class = Virtual
+ _fact_ids = set([
+ 'virtualization_type',
+ 'virtualization_role',
+ 'virtualization_tech_guest',
+ 'virtualization_tech_host',
+ ]) # type: t.Set[str]
+
+ def collect(self, module=None, collected_facts=None):
+ collected_facts = collected_facts or {}
+ if not module:
+ return {}
+
+ # Network munges cached_facts by side effect, so give it a copy
+ facts_obj = self._fact_class(module)
+
+ facts_dict = facts_obj.populate(collected_facts=collected_facts)
+
+ return facts_dict
diff --git a/lib/ansible/module_utils/facts/virtual/dragonfly.py b/lib/ansible/module_utils/facts/virtual/dragonfly.py
new file mode 100644
index 0000000..b176f8b
--- /dev/null
+++ b/lib/ansible/module_utils/facts/virtual/dragonfly.py
@@ -0,0 +1,25 @@
+# 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
+
+from ansible.module_utils.facts.virtual.freebsd import FreeBSDVirtual, VirtualCollector
+
+
+class DragonFlyVirtualCollector(VirtualCollector):
+ # Note the _fact_class impl is actually the FreeBSDVirtual impl
+ _fact_class = FreeBSDVirtual
+ _platform = 'DragonFly'
diff --git a/lib/ansible/module_utils/facts/virtual/freebsd.py b/lib/ansible/module_utils/facts/virtual/freebsd.py
new file mode 100644
index 0000000..7062d01
--- /dev/null
+++ b/lib/ansible/module_utils/facts/virtual/freebsd.py
@@ -0,0 +1,79 @@
+# 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
+
+from ansible.module_utils.facts.virtual.base import Virtual, VirtualCollector
+from ansible.module_utils.facts.virtual.sysctl import VirtualSysctlDetectionMixin
+
+
+class FreeBSDVirtual(Virtual, VirtualSysctlDetectionMixin):
+ """
+ This is a FreeBSD-specific subclass of Virtual. It defines
+ - virtualization_type
+ - virtualization_role
+ """
+ platform = 'FreeBSD'
+
+ def get_virtual_facts(self):
+ virtual_facts = {}
+ host_tech = set()
+ guest_tech = set()
+
+ # Set empty values as default
+ virtual_facts['virtualization_type'] = ''
+ virtual_facts['virtualization_role'] = ''
+
+ if os.path.exists('/dev/xen/xenstore'):
+ guest_tech.add('xen')
+ virtual_facts['virtualization_type'] = 'xen'
+ virtual_facts['virtualization_role'] = 'guest'
+
+ kern_vm_guest = self.detect_virt_product('kern.vm_guest')
+ guest_tech.update(kern_vm_guest['virtualization_tech_guest'])
+ host_tech.update(kern_vm_guest['virtualization_tech_host'])
+
+ hw_hv_vendor = self.detect_virt_product('hw.hv_vendor')
+ guest_tech.update(hw_hv_vendor['virtualization_tech_guest'])
+ host_tech.update(hw_hv_vendor['virtualization_tech_host'])
+
+ sec_jail_jailed = self.detect_virt_product('security.jail.jailed')
+ guest_tech.update(sec_jail_jailed['virtualization_tech_guest'])
+ host_tech.update(sec_jail_jailed['virtualization_tech_host'])
+
+ if virtual_facts['virtualization_type'] == '':
+ sysctl = kern_vm_guest or hw_hv_vendor or sec_jail_jailed
+ # We call update here, then re-set virtualization_tech_host/guest
+ # later.
+ virtual_facts.update(sysctl)
+
+ virtual_vendor_facts = self.detect_virt_vendor('hw.model')
+ guest_tech.update(virtual_vendor_facts['virtualization_tech_guest'])
+ host_tech.update(virtual_vendor_facts['virtualization_tech_host'])
+
+ if virtual_facts['virtualization_type'] == '':
+ virtual_facts.update(virtual_vendor_facts)
+
+ virtual_facts['virtualization_tech_guest'] = guest_tech
+ virtual_facts['virtualization_tech_host'] = host_tech
+ return virtual_facts
+
+
+class FreeBSDVirtualCollector(VirtualCollector):
+ _fact_class = FreeBSDVirtual
+ _platform = 'FreeBSD'
diff --git a/lib/ansible/module_utils/facts/virtual/hpux.py b/lib/ansible/module_utils/facts/virtual/hpux.py
new file mode 100644
index 0000000..1057482
--- /dev/null
+++ b/lib/ansible/module_utils/facts/virtual/hpux.py
@@ -0,0 +1,72 @@
+# 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 re
+
+from ansible.module_utils.facts.virtual.base import Virtual, VirtualCollector
+
+
+class HPUXVirtual(Virtual):
+ """
+ This is a HP-UX specific subclass of Virtual. It defines
+ - virtualization_type
+ - virtualization_role
+ """
+ platform = 'HP-UX'
+
+ def get_virtual_facts(self):
+ virtual_facts = {}
+ host_tech = set()
+ guest_tech = set()
+
+ if os.path.exists('/usr/sbin/vecheck'):
+ rc, out, err = self.module.run_command("/usr/sbin/vecheck")
+ if rc == 0:
+ guest_tech.add('HP vPar')
+ virtual_facts['virtualization_type'] = 'guest'
+ virtual_facts['virtualization_role'] = 'HP vPar'
+ if os.path.exists('/opt/hpvm/bin/hpvminfo'):
+ rc, out, err = self.module.run_command("/opt/hpvm/bin/hpvminfo")
+ if rc == 0 and re.match('.*Running.*HPVM vPar.*', out):
+ guest_tech.add('HPVM vPar')
+ virtual_facts['virtualization_type'] = 'guest'
+ virtual_facts['virtualization_role'] = 'HPVM vPar'
+ elif rc == 0 and re.match('.*Running.*HPVM guest.*', out):
+ guest_tech.add('HPVM IVM')
+ virtual_facts['virtualization_type'] = 'guest'
+ virtual_facts['virtualization_role'] = 'HPVM IVM'
+ elif rc == 0 and re.match('.*Running.*HPVM host.*', out):
+ guest_tech.add('HPVM')
+ virtual_facts['virtualization_type'] = 'host'
+ virtual_facts['virtualization_role'] = 'HPVM'
+ if os.path.exists('/usr/sbin/parstatus'):
+ rc, out, err = self.module.run_command("/usr/sbin/parstatus")
+ if rc == 0:
+ guest_tech.add('HP nPar')
+ virtual_facts['virtualization_type'] = 'guest'
+ virtual_facts['virtualization_role'] = 'HP nPar'
+
+ virtual_facts['virtualization_tech_guest'] = guest_tech
+ virtual_facts['virtualization_tech_host'] = host_tech
+ return virtual_facts
+
+
+class HPUXVirtualCollector(VirtualCollector):
+ _fact_class = HPUXVirtual
+ _platform = 'HP-UX'
diff --git a/lib/ansible/module_utils/facts/virtual/linux.py b/lib/ansible/module_utils/facts/virtual/linux.py
new file mode 100644
index 0000000..31fa061
--- /dev/null
+++ b/lib/ansible/module_utils/facts/virtual/linux.py
@@ -0,0 +1,405 @@
+# 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 os
+import re
+
+from ansible.module_utils.facts.virtual.base import Virtual, VirtualCollector
+from ansible.module_utils.facts.utils import get_file_content, get_file_lines
+
+
+class LinuxVirtual(Virtual):
+ """
+ This is a Linux-specific subclass of Virtual. It defines
+ - virtualization_type
+ - virtualization_role
+ """
+ platform = 'Linux'
+
+ # For more information, check: http://people.redhat.com/~rjones/virt-what/
+ def get_virtual_facts(self):
+ virtual_facts = {}
+
+ # We want to maintain compatibility with the old "virtualization_type"
+ # and "virtualization_role" entries, so we need to track if we found
+ # them. We won't return them until the end, but if we found them early,
+ # we should avoid updating them again.
+ found_virt = False
+
+ # But as we go along, we also want to track virt tech the new way.
+ host_tech = set()
+ guest_tech = set()
+
+ # lxc/docker
+ if os.path.exists('/proc/1/cgroup'):
+ for line in get_file_lines('/proc/1/cgroup'):
+ if re.search(r'/docker(/|-[0-9a-f]+\.scope)', line):
+ guest_tech.add('docker')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'docker'
+ virtual_facts['virtualization_role'] = 'guest'
+ found_virt = True
+ if re.search('/lxc/', line) or re.search('/machine.slice/machine-lxc', line):
+ guest_tech.add('lxc')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'lxc'
+ virtual_facts['virtualization_role'] = 'guest'
+ found_virt = True
+ if re.search('/system.slice/containerd.service', line):
+ guest_tech.add('containerd')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'containerd'
+ virtual_facts['virtualization_role'] = 'guest'
+ found_virt = True
+
+ # lxc does not always appear in cgroups anymore but sets 'container=lxc' environment var, requires root privs
+ if os.path.exists('/proc/1/environ'):
+ for line in get_file_lines('/proc/1/environ', line_sep='\x00'):
+ if re.search('container=lxc', line):
+ guest_tech.add('lxc')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'lxc'
+ virtual_facts['virtualization_role'] = 'guest'
+ found_virt = True
+ if re.search('container=podman', line):
+ guest_tech.add('podman')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'podman'
+ virtual_facts['virtualization_role'] = 'guest'
+ found_virt = True
+ if re.search('^container=.', line):
+ guest_tech.add('container')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'container'
+ virtual_facts['virtualization_role'] = 'guest'
+ found_virt = True
+
+ if os.path.exists('/proc/vz') and not os.path.exists('/proc/lve'):
+ virtual_facts['virtualization_type'] = 'openvz'
+ if os.path.exists('/proc/bc'):
+ host_tech.add('openvz')
+ if not found_virt:
+ virtual_facts['virtualization_role'] = 'host'
+ else:
+ guest_tech.add('openvz')
+ if not found_virt:
+ virtual_facts['virtualization_role'] = 'guest'
+ found_virt = True
+
+ systemd_container = get_file_content('/run/systemd/container')
+ if systemd_container:
+ guest_tech.add(systemd_container)
+ if not found_virt:
+ virtual_facts['virtualization_type'] = systemd_container
+ virtual_facts['virtualization_role'] = 'guest'
+ found_virt = True
+
+ # If docker/containerd has a custom cgroup parent, checking /proc/1/cgroup (above) might fail.
+ # https://docs.docker.com/engine/reference/commandline/dockerd/#default-cgroup-parent
+ # Fallback to more rudimentary checks.
+ if os.path.exists('/.dockerenv') or os.path.exists('/.dockerinit'):
+ guest_tech.add('docker')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'docker'
+ virtual_facts['virtualization_role'] = 'guest'
+ found_virt = True
+
+ # ensure 'container' guest_tech is appropriately set
+ if guest_tech.intersection(set(['docker', 'lxc', 'podman', 'openvz', 'containerd'])) or systemd_container:
+ guest_tech.add('container')
+
+ if os.path.exists("/proc/xen"):
+ is_xen_host = False
+ try:
+ for line in get_file_lines('/proc/xen/capabilities'):
+ if "control_d" in line:
+ is_xen_host = True
+ except IOError:
+ pass
+
+ if is_xen_host:
+ host_tech.add('xen')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'xen'
+ virtual_facts['virtualization_role'] = 'host'
+ else:
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'xen'
+ virtual_facts['virtualization_role'] = 'guest'
+ found_virt = True
+
+ # assume guest for this block
+ if not found_virt:
+ virtual_facts['virtualization_role'] = 'guest'
+
+ product_name = get_file_content('/sys/devices/virtual/dmi/id/product_name')
+ sys_vendor = get_file_content('/sys/devices/virtual/dmi/id/sys_vendor')
+ product_family = get_file_content('/sys/devices/virtual/dmi/id/product_family')
+
+ if product_name in ('KVM', 'KVM Server', 'Bochs', 'AHV'):
+ guest_tech.add('kvm')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'kvm'
+ found_virt = True
+
+ if sys_vendor == 'oVirt':
+ guest_tech.add('oVirt')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'oVirt'
+ found_virt = True
+
+ if sys_vendor == 'Red Hat':
+ if product_family == 'RHV':
+ guest_tech.add('RHV')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'RHV'
+ found_virt = True
+ elif product_name == 'RHEV Hypervisor':
+ guest_tech.add('RHEV')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'RHEV'
+ found_virt = True
+
+ if product_name in ('VMware Virtual Platform', 'VMware7,1'):
+ guest_tech.add('VMware')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'VMware'
+ found_virt = True
+
+ if product_name in ('OpenStack Compute', 'OpenStack Nova'):
+ guest_tech.add('openstack')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'openstack'
+ found_virt = True
+
+ bios_vendor = get_file_content('/sys/devices/virtual/dmi/id/bios_vendor')
+
+ if bios_vendor == 'Xen':
+ guest_tech.add('xen')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'xen'
+ found_virt = True
+
+ if bios_vendor == 'innotek GmbH':
+ guest_tech.add('virtualbox')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'virtualbox'
+ found_virt = True
+
+ if bios_vendor in ('Amazon EC2', 'DigitalOcean', 'Hetzner'):
+ guest_tech.add('kvm')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'kvm'
+ found_virt = True
+
+ KVM_SYS_VENDORS = ('QEMU', 'Amazon EC2', 'DigitalOcean', 'Google', 'Scaleway', 'Nutanix')
+ if sys_vendor in KVM_SYS_VENDORS:
+ guest_tech.add('kvm')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'kvm'
+ found_virt = True
+
+ if sys_vendor == 'KubeVirt':
+ guest_tech.add('KubeVirt')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'KubeVirt'
+ found_virt = True
+
+ # FIXME: This does also match hyperv
+ if sys_vendor == 'Microsoft Corporation':
+ guest_tech.add('VirtualPC')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'VirtualPC'
+ found_virt = True
+
+ if sys_vendor == 'Parallels Software International Inc.':
+ guest_tech.add('parallels')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'parallels'
+ found_virt = True
+
+ if sys_vendor == 'OpenStack Foundation':
+ guest_tech.add('openstack')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'openstack'
+ found_virt = True
+
+ # unassume guest
+ if not found_virt:
+ del virtual_facts['virtualization_role']
+
+ if os.path.exists('/proc/self/status'):
+ for line in get_file_lines('/proc/self/status'):
+ if re.match(r'^VxID:\s+\d+', line):
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'linux_vserver'
+ if re.match(r'^VxID:\s+0', line):
+ host_tech.add('linux_vserver')
+ if not found_virt:
+ virtual_facts['virtualization_role'] = 'host'
+ else:
+ guest_tech.add('linux_vserver')
+ if not found_virt:
+ virtual_facts['virtualization_role'] = 'guest'
+ found_virt = True
+
+ if os.path.exists('/proc/cpuinfo'):
+ for line in get_file_lines('/proc/cpuinfo'):
+ if re.match('^model name.*QEMU Virtual CPU', line):
+ guest_tech.add('kvm')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'kvm'
+ elif re.match('^vendor_id.*User Mode Linux', line):
+ guest_tech.add('uml')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'uml'
+ elif re.match('^model name.*UML', line):
+ guest_tech.add('uml')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'uml'
+ elif re.match('^machine.*CHRP IBM pSeries .emulated by qemu.', line):
+ guest_tech.add('kvm')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'kvm'
+ elif re.match('^vendor_id.*PowerVM Lx86', line):
+ guest_tech.add('powervm_lx86')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'powervm_lx86'
+ elif re.match('^vendor_id.*IBM/S390', line):
+ guest_tech.add('PR/SM')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'PR/SM'
+ lscpu = self.module.get_bin_path('lscpu')
+ if lscpu:
+ rc, out, err = self.module.run_command(["lscpu"])
+ if rc == 0:
+ for line in out.splitlines():
+ data = line.split(":", 1)
+ key = data[0].strip()
+ if key == 'Hypervisor':
+ tech = data[1].strip()
+ guest_tech.add(tech)
+ if not found_virt:
+ virtual_facts['virtualization_type'] = tech
+ else:
+ guest_tech.add('ibm_systemz')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'ibm_systemz'
+ else:
+ continue
+ if virtual_facts['virtualization_type'] == 'PR/SM':
+ if not found_virt:
+ virtual_facts['virtualization_role'] = 'LPAR'
+ else:
+ if not found_virt:
+ virtual_facts['virtualization_role'] = 'guest'
+ if not found_virt:
+ found_virt = True
+
+ # Beware that we can have both kvm and virtualbox running on a single system
+ if os.path.exists("/proc/modules") and os.access('/proc/modules', os.R_OK):
+ modules = []
+ for line in get_file_lines("/proc/modules"):
+ data = line.split(" ", 1)
+ modules.append(data[0])
+
+ if 'kvm' in modules:
+ host_tech.add('kvm')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'kvm'
+ virtual_facts['virtualization_role'] = 'host'
+
+ if os.path.isdir('/rhev/'):
+ # Check whether this is a RHEV hypervisor (is vdsm running ?)
+ for f in glob.glob('/proc/[0-9]*/comm'):
+ try:
+ with open(f) as virt_fh:
+ comm_content = virt_fh.read().rstrip()
+
+ if comm_content in ('vdsm', 'vdsmd'):
+ # We add both kvm and RHEV to host_tech in this case.
+ # It's accurate. RHEV uses KVM.
+ host_tech.add('RHEV')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'RHEV'
+ break
+ except Exception:
+ pass
+
+ found_virt = True
+
+ if 'vboxdrv' in modules:
+ host_tech.add('virtualbox')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'virtualbox'
+ virtual_facts['virtualization_role'] = 'host'
+ found_virt = True
+
+ if 'virtio' in modules:
+ host_tech.add('kvm')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'kvm'
+ virtual_facts['virtualization_role'] = 'guest'
+ found_virt = True
+
+ # In older Linux Kernel versions, /sys filesystem is not available
+ # dmidecode is the safest option to parse virtualization related values
+ dmi_bin = self.module.get_bin_path('dmidecode')
+ # We still want to continue even if dmidecode is not available
+ if dmi_bin is not None:
+ (rc, out, err) = self.module.run_command('%s -s system-product-name' % dmi_bin)
+ if rc == 0:
+ # Strip out commented lines (specific dmidecode output)
+ vendor_name = ''.join([line.strip() for line in out.splitlines() if not line.startswith('#')])
+ if vendor_name.startswith('VMware'):
+ guest_tech.add('VMware')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'VMware'
+ virtual_facts['virtualization_role'] = 'guest'
+ found_virt = True
+
+ if 'BHYVE' in out:
+ guest_tech.add('bhyve')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'bhyve'
+ virtual_facts['virtualization_role'] = 'guest'
+ found_virt = True
+
+ if os.path.exists('/dev/kvm'):
+ host_tech.add('kvm')
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'kvm'
+ virtual_facts['virtualization_role'] = 'host'
+ found_virt = True
+
+ # If none of the above matches, return 'NA' for virtualization_type
+ # and virtualization_role. This allows for proper grouping.
+ if not found_virt:
+ virtual_facts['virtualization_type'] = 'NA'
+ virtual_facts['virtualization_role'] = 'NA'
+ found_virt = True
+
+ virtual_facts['virtualization_tech_guest'] = guest_tech
+ virtual_facts['virtualization_tech_host'] = host_tech
+ return virtual_facts
+
+
+class LinuxVirtualCollector(VirtualCollector):
+ _fact_class = LinuxVirtual
+ _platform = 'Linux'
diff --git a/lib/ansible/module_utils/facts/virtual/netbsd.py b/lib/ansible/module_utils/facts/virtual/netbsd.py
new file mode 100644
index 0000000..b4ef14e
--- /dev/null
+++ b/lib/ansible/module_utils/facts/virtual/netbsd.py
@@ -0,0 +1,73 @@
+# 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
+
+from ansible.module_utils.facts.virtual.base import Virtual, VirtualCollector
+from ansible.module_utils.facts.virtual.sysctl import VirtualSysctlDetectionMixin
+
+
+class NetBSDVirtual(Virtual, VirtualSysctlDetectionMixin):
+ platform = 'NetBSD'
+
+ def get_virtual_facts(self):
+ virtual_facts = {}
+ host_tech = set()
+ guest_tech = set()
+
+ # Set empty values as default
+ virtual_facts['virtualization_type'] = ''
+ virtual_facts['virtualization_role'] = ''
+
+ virtual_product_facts = self.detect_virt_product('machdep.dmi.system-product')
+ guest_tech.update(virtual_product_facts['virtualization_tech_guest'])
+ host_tech.update(virtual_product_facts['virtualization_tech_host'])
+ virtual_facts.update(virtual_product_facts)
+
+ virtual_vendor_facts = self.detect_virt_vendor('machdep.dmi.system-vendor')
+ guest_tech.update(virtual_vendor_facts['virtualization_tech_guest'])
+ host_tech.update(virtual_vendor_facts['virtualization_tech_host'])
+
+ if virtual_facts['virtualization_type'] == '':
+ virtual_facts.update(virtual_vendor_facts)
+
+ # The above logic is tried first for backwards compatibility. If
+ # something above matches, use it. Otherwise if the result is still
+ # empty, try machdep.hypervisor.
+ virtual_vendor_facts = self.detect_virt_vendor('machdep.hypervisor')
+ guest_tech.update(virtual_vendor_facts['virtualization_tech_guest'])
+ host_tech.update(virtual_vendor_facts['virtualization_tech_host'])
+
+ if virtual_facts['virtualization_type'] == '':
+ virtual_facts.update(virtual_vendor_facts)
+
+ if os.path.exists('/dev/xencons'):
+ guest_tech.add('xen')
+
+ if virtual_facts['virtualization_type'] == '':
+ virtual_facts['virtualization_type'] = 'xen'
+ virtual_facts['virtualization_role'] = 'guest'
+
+ virtual_facts['virtualization_tech_guest'] = guest_tech
+ virtual_facts['virtualization_tech_host'] = host_tech
+ return virtual_facts
+
+
+class NetBSDVirtualCollector(VirtualCollector):
+ _fact_class = NetBSDVirtual
+ _platform = 'NetBSD'
diff --git a/lib/ansible/module_utils/facts/virtual/openbsd.py b/lib/ansible/module_utils/facts/virtual/openbsd.py
new file mode 100644
index 0000000..c449028
--- /dev/null
+++ b/lib/ansible/module_utils/facts/virtual/openbsd.py
@@ -0,0 +1,74 @@
+# 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
+
+from ansible.module_utils.facts.virtual.base import Virtual, VirtualCollector
+from ansible.module_utils.facts.virtual.sysctl import VirtualSysctlDetectionMixin
+
+from ansible.module_utils.facts.utils import get_file_content
+
+
+class OpenBSDVirtual(Virtual, VirtualSysctlDetectionMixin):
+ """
+ This is a OpenBSD-specific subclass of Virtual. It defines
+ - virtualization_type
+ - virtualization_role
+ """
+ platform = 'OpenBSD'
+ DMESG_BOOT = '/var/run/dmesg.boot'
+
+ def get_virtual_facts(self):
+ virtual_facts = {}
+ host_tech = set()
+ guest_tech = set()
+
+ # Set empty values as default
+ virtual_facts['virtualization_type'] = ''
+ virtual_facts['virtualization_role'] = ''
+
+ virtual_product_facts = self.detect_virt_product('hw.product')
+ guest_tech.update(virtual_product_facts['virtualization_tech_guest'])
+ host_tech.update(virtual_product_facts['virtualization_tech_host'])
+ virtual_facts.update(virtual_product_facts)
+
+ virtual_vendor_facts = self.detect_virt_vendor('hw.vendor')
+ guest_tech.update(virtual_vendor_facts['virtualization_tech_guest'])
+ host_tech.update(virtual_vendor_facts['virtualization_tech_host'])
+
+ if virtual_facts['virtualization_type'] == '':
+ virtual_facts.update(virtual_vendor_facts)
+
+ # Check the dmesg if vmm(4) attached, indicating the host is
+ # capable of virtualization.
+ dmesg_boot = get_file_content(OpenBSDVirtual.DMESG_BOOT)
+ for line in dmesg_boot.splitlines():
+ match = re.match('^vmm0 at mainbus0: (SVM/RVI|VMX/EPT)$', line)
+ if match:
+ host_tech.add('vmm')
+ virtual_facts['virtualization_type'] = 'vmm'
+ virtual_facts['virtualization_role'] = 'host'
+
+ virtual_facts['virtualization_tech_guest'] = guest_tech
+ virtual_facts['virtualization_tech_host'] = host_tech
+ return virtual_facts
+
+
+class OpenBSDVirtualCollector(VirtualCollector):
+ _fact_class = OpenBSDVirtual
+ _platform = 'OpenBSD'
diff --git a/lib/ansible/module_utils/facts/virtual/sunos.py b/lib/ansible/module_utils/facts/virtual/sunos.py
new file mode 100644
index 0000000..1e92677
--- /dev/null
+++ b/lib/ansible/module_utils/facts/virtual/sunos.py
@@ -0,0 +1,139 @@
+# 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
+
+from ansible.module_utils.facts.virtual.base import Virtual, VirtualCollector
+
+
+class SunOSVirtual(Virtual):
+ """
+ This is a SunOS-specific subclass of Virtual. It defines
+ - virtualization_type
+ - virtualization_role
+ - container
+ """
+ platform = 'SunOS'
+
+ def get_virtual_facts(self):
+ virtual_facts = {}
+ host_tech = set()
+ guest_tech = set()
+
+ # Check if it's a zone
+ zonename = self.module.get_bin_path('zonename')
+ if zonename:
+ rc, out, err = self.module.run_command(zonename)
+ if rc == 0:
+ if out.rstrip() == "global":
+ host_tech.add('zone')
+ else:
+ guest_tech.add('zone')
+ virtual_facts['container'] = 'zone'
+
+ # Check if it's a branded zone (i.e. Solaris 8/9 zone)
+ if os.path.isdir('/.SUNWnative'):
+ guest_tech.add('zone')
+ virtual_facts['container'] = 'zone'
+
+ # If it's a zone check if we can detect if our global zone is itself virtualized.
+ # Relies on the "guest tools" (e.g. vmware tools) to be installed
+ if 'container' in virtual_facts and virtual_facts['container'] == 'zone':
+ modinfo = self.module.get_bin_path('modinfo')
+ if modinfo:
+ rc, out, err = self.module.run_command(modinfo)
+ if rc == 0:
+ for line in out.splitlines():
+ if 'VMware' in line:
+ guest_tech.add('vmware')
+ virtual_facts['virtualization_type'] = 'vmware'
+ virtual_facts['virtualization_role'] = 'guest'
+ if 'VirtualBox' in line:
+ guest_tech.add('virtualbox')
+ virtual_facts['virtualization_type'] = 'virtualbox'
+ virtual_facts['virtualization_role'] = 'guest'
+
+ if os.path.exists('/proc/vz'):
+ guest_tech.add('virtuozzo')
+ virtual_facts['virtualization_type'] = 'virtuozzo'
+ virtual_facts['virtualization_role'] = 'guest'
+
+ # Detect domaining on Sparc hardware
+ virtinfo = self.module.get_bin_path('virtinfo')
+ if virtinfo:
+ # The output of virtinfo is different whether we are on a machine with logical
+ # domains ('LDoms') on a T-series or domains ('Domains') on a M-series. Try LDoms first.
+ rc, out, err = self.module.run_command("/usr/sbin/virtinfo -p")
+ # The output contains multiple lines with different keys like this:
+ # DOMAINROLE|impl=LDoms|control=false|io=false|service=false|root=false
+ # The output may also be not formatted and the returncode is set to 0 regardless of the error condition:
+ # virtinfo can only be run from the global zone
+ if rc == 0:
+ try:
+ for line in out.splitlines():
+ fields = line.split('|')
+ if fields[0] == 'DOMAINROLE' and fields[1] == 'impl=LDoms':
+ guest_tech.add('ldom')
+ virtual_facts['virtualization_type'] = 'ldom'
+ virtual_facts['virtualization_role'] = 'guest'
+ hostfeatures = []
+ for field in fields[2:]:
+ arg = field.split('=')
+ if arg[1] == 'true':
+ hostfeatures.append(arg[0])
+ if len(hostfeatures) > 0:
+ virtual_facts['virtualization_role'] = 'host (' + ','.join(hostfeatures) + ')'
+ except ValueError:
+ pass
+
+ else:
+ smbios = self.module.get_bin_path('smbios')
+ if not smbios:
+ return
+ rc, out, err = self.module.run_command(smbios)
+ if rc == 0:
+ for line in out.splitlines():
+ if 'VMware' in line:
+ guest_tech.add('vmware')
+ virtual_facts['virtualization_type'] = 'vmware'
+ virtual_facts['virtualization_role'] = 'guest'
+ elif 'Parallels' in line:
+ guest_tech.add('parallels')
+ virtual_facts['virtualization_type'] = 'parallels'
+ virtual_facts['virtualization_role'] = 'guest'
+ elif 'VirtualBox' in line:
+ guest_tech.add('virtualbox')
+ virtual_facts['virtualization_type'] = 'virtualbox'
+ virtual_facts['virtualization_role'] = 'guest'
+ elif 'HVM domU' in line:
+ guest_tech.add('xen')
+ virtual_facts['virtualization_type'] = 'xen'
+ virtual_facts['virtualization_role'] = 'guest'
+ elif 'KVM' in line:
+ guest_tech.add('kvm')
+ virtual_facts['virtualization_type'] = 'kvm'
+ virtual_facts['virtualization_role'] = 'guest'
+
+ virtual_facts['virtualization_tech_guest'] = guest_tech
+ virtual_facts['virtualization_tech_host'] = host_tech
+ return virtual_facts
+
+
+class SunOSVirtualCollector(VirtualCollector):
+ _fact_class = SunOSVirtual
+ _platform = 'SunOS'
diff --git a/lib/ansible/module_utils/facts/virtual/sysctl.py b/lib/ansible/module_utils/facts/virtual/sysctl.py
new file mode 100644
index 0000000..1c7b2b3
--- /dev/null
+++ b/lib/ansible/module_utils/facts/virtual/sysctl.py
@@ -0,0 +1,112 @@
+# 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
+
+
+class VirtualSysctlDetectionMixin(object):
+ def detect_sysctl(self):
+ self.sysctl_path = self.module.get_bin_path('sysctl')
+
+ def detect_virt_product(self, key):
+ virtual_product_facts = {}
+ host_tech = set()
+ guest_tech = set()
+
+ # We do similar to what we do in linux.py -- We want to allow multiple
+ # virt techs to show up, but maintain compatibility, so we have to track
+ # when we would have stopped, even though now we go through everything.
+ found_virt = False
+
+ self.detect_sysctl()
+ if self.sysctl_path:
+ rc, out, err = self.module.run_command("%s -n %s" % (self.sysctl_path, key))
+ if rc == 0:
+ if re.match('(KVM|kvm|Bochs|SmartDC).*', out):
+ guest_tech.add('kvm')
+ if not found_virt:
+ virtual_product_facts['virtualization_type'] = 'kvm'
+ virtual_product_facts['virtualization_role'] = 'guest'
+ found_virt = True
+ if re.match('.*VMware.*', out):
+ guest_tech.add('VMware')
+ if not found_virt:
+ virtual_product_facts['virtualization_type'] = 'VMware'
+ virtual_product_facts['virtualization_role'] = 'guest'
+ found_virt = True
+ if out.rstrip() == 'VirtualBox':
+ guest_tech.add('virtualbox')
+ if not found_virt:
+ virtual_product_facts['virtualization_type'] = 'virtualbox'
+ virtual_product_facts['virtualization_role'] = 'guest'
+ found_virt = True
+ if re.match('(HVM domU|XenPVH|XenPV|XenPVHVM).*', out):
+ guest_tech.add('xen')
+ if not found_virt:
+ virtual_product_facts['virtualization_type'] = 'xen'
+ virtual_product_facts['virtualization_role'] = 'guest'
+ found_virt = True
+ if out.rstrip() == 'Hyper-V':
+ guest_tech.add('Hyper-V')
+ if not found_virt:
+ virtual_product_facts['virtualization_type'] = 'Hyper-V'
+ virtual_product_facts['virtualization_role'] = 'guest'
+ found_virt = True
+ if out.rstrip() == 'Parallels':
+ guest_tech.add('parallels')
+ if not found_virt:
+ virtual_product_facts['virtualization_type'] = 'parallels'
+ virtual_product_facts['virtualization_role'] = 'guest'
+ found_virt = True
+ if out.rstrip() == 'RHEV Hypervisor':
+ guest_tech.add('RHEV')
+ if not found_virt:
+ virtual_product_facts['virtualization_type'] = 'RHEV'
+ virtual_product_facts['virtualization_role'] = 'guest'
+ found_virt = True
+ if (key == 'security.jail.jailed') and (out.rstrip() == '1'):
+ guest_tech.add('jails')
+ if not found_virt:
+ virtual_product_facts['virtualization_type'] = 'jails'
+ virtual_product_facts['virtualization_role'] = 'guest'
+ found_virt = True
+
+ virtual_product_facts['virtualization_tech_guest'] = guest_tech
+ virtual_product_facts['virtualization_tech_host'] = host_tech
+ return virtual_product_facts
+
+ def detect_virt_vendor(self, key):
+ virtual_vendor_facts = {}
+ host_tech = set()
+ guest_tech = set()
+ self.detect_sysctl()
+ if self.sysctl_path:
+ rc, out, err = self.module.run_command("%s -n %s" % (self.sysctl_path, key))
+ if rc == 0:
+ if out.rstrip() == 'QEMU':
+ guest_tech.add('kvm')
+ virtual_vendor_facts['virtualization_type'] = 'kvm'
+ virtual_vendor_facts['virtualization_role'] = 'guest'
+ if out.rstrip() == 'OpenBSD':
+ guest_tech.add('vmm')
+ virtual_vendor_facts['virtualization_type'] = 'vmm'
+ virtual_vendor_facts['virtualization_role'] = 'guest'
+
+ virtual_vendor_facts['virtualization_tech_guest'] = guest_tech
+ virtual_vendor_facts['virtualization_tech_host'] = host_tech
+ return virtual_vendor_facts