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