Facts Refresh (2.4 roadmap) (#23012)

Facts Refresh (2.4 roadmap)

This commit implements most of the 2.4 roadmap 'Facts Refresh'
- move facts.py to facts/__init__.py
- move facts Distribution() to its own class
- add a facts/utils.py
- move get_file_content and get_uname_version to facts/utils.py
- move Facts() class from facts/__init__ to facts/facts.py
- mv get_file_lines to facts/utils.py
- mv Ohai()/Facter() class to facts/ohai.py and facter.py
- Start moving fact Hardware() classes to facts/hardware/*.py
- mv HPUX() hardware class to facts/hardware/hpux.py
- move SunOSHardware() fact class to facts/hardware/sunos.py
- move OpenBSDHardware() class to facts/hardware/openbsd.py
- mv FreeBsdHardware() and DragonFlyHardware() to facts/hardware/
- mv NetBSDHardware() to facts/hardware/netbsd.py
- mv Darwin() hardware class to facts/hardware/darwin.py
- pep8/etc cleanups on facts/hardware/*.py
- Mv network facts classes to facts/network/*.py
- mv Virtual fact classes to facts/virtual
- mv Hardware.get_sysctl to facts/sysctl.py:get_sysctl

- Also mv get_uname_version from facts/utils.py -> distribution.py
  since distribution.py is the only thing using it.

- add collector.py with new BaseFactCollector
- add a subclass for AnsibleFactCollector
- hook up dict key munging FactNamespaces
- add some test cases for testing the names of facts
- mv timeout stuff to facts.timeout

- rm ansible_facts()/get_all_facts() etc

- Instead of calling facts.ansible_facts(), fact collection
  api used by setup.py is now to create an AnsibleFactCollector()
  and call it's collect method.

- replace Facts.get_user_facts with UserFactCollector
- add a 'systems' facts package, mv UserFactCollector there
- mv get_dns_facts to DnsFactCollector
- mv get_env_facts to EnvFactCollector
- include the timeout length in exception message

- modules and module_utils that use AnsibleFactCollector
  can now theoretically set the 'valid_subsets'

  May be useful for network facts module that currently have
  to reimplement a good chunk of facts.py to get gather_subsets
  to work.

- get_local_facts -> system/LocalFactCollector
- get_date_time -> system/date_time.py
- get_fips_facts -> system/fips.py
- get_caps_facts() -> system/caps.py
- get_apparmor_facts -> system/apparmor.py
- get_selinux_facts -> system/selinux.py
- get_lsb_facts -> system/lsb.py
- get_service_mgr_facts -> system/service_mgr.py
- Facts.is_systemd_managed ->  system/service_mgr.py
- get_pkg_mgr_facts -> system/pkg_mgr.py
- Facts()._get_mount_size_facts() -> facts.utils.get_mount_size()

- add unit test for EnvFactCollector
- add a test case for minimal gather_subsets
- add test case for collect_ids
- Make gather_subset match existing behavior or '!all'

    If 'gather_subset' is provided as '!all', the existing behavior
    (in 2.2/2.3) is that means 'dont collect any facts except those
    from the Facts() class'. So 'skip everything except
    'apparmor', 'caps', 'date_time', 'env', 'fips', 'local', 'lsb',
    'pkg_mgr', 'python', 'selinux', 'service_mgr', 'user', 'platform', etc.

    The new facts setup was making '!all' mean no facts at all, since
    it can add/exclude at a finer granularity. Since that makes more
    sense for the ansible collector, and the set of minimal facts to
    collect is really more up to setup.py to decide we do just that.

    So if setup.py needs to always collect some gather_subset, even
    on !all, setup.py needs to have the that subset added to the
    list it passes as minimal_gather_subset.

    This should fix some intg tests that assume '!all' means that
    some facts are still collected (user info and env for example).

    If we want to make setup.py collect a more minimal set, we can do that.

- force facts_dicts.keys() to a list so py3 works
- split fact collector tests to test_collectors.py

- convert Facter(Facts) -> other/facter.py:FacterFactCollector

- add FactCollector.collect_with_namespace()

    regular .collect() will return a dict with the key names
    using the base names ('ip_address', 'service_mgr' etc)

    .collect_with_namespace() will return a dict where the key names
    have been transformed with the collectors namespace, if there is
    one. For most, this means a namespace that adds 'ansible_' to the
    start of the key name.

    For 'FacterFactCollector', the namespace transforms the key to
    'facter_*'.

- add test cases for collect_with_namespace

- move all the concrete 'which facts does setup.py' stuff to setup.py

    The caller of AnsibleFactCollector.from_gather_subset() needs to
    pass in the list of collector classes now.

- update system/setup.py to import all of the fact classes and pass
  in that list.
- split the Distribution fact class up a bit

    extracted the 'distro release' file handling (ie, linux
    boxes with /etc/release, /etc/os-release etc) into its
    own class.
- extract get_cmdline_facts -> cmdline.py
- extract get_public_ssh_host_keys -> system/ssh_pub_keys.py
- extract get_platform_facts -> system/platform.py

  platform.py may be a good candidate for further splitting.

- rm test for plain Facts() base class
- let the base class for Collector unit tests provide collected_facts

    some Collectors and/or their migrated Facts() subsclasses need
    to look at facts collected by other modules ('ansible_architecture'
    the main one...).

    Collector.collect() has the collected_facts arg for this, so add
    a class variable to BaseFactsTest so we can specify it.

- mv Ohai to other/ohai.py and convert to Collector
- update hardware/*.py to return facts (no side effects)

- mv AnsibleFactCollector to setup.py
- extra collector class gathering to module method in
  facts/__init__.py (collector_classes_from_gather_subset)
- add a CollectorMetaDataCollector collector used to provide
  the 'gather_setup' fact
- add unit test module for 'setup' module
  (test/units/modules/system/setup.py)

- Collector init now doesnt need a module, but collect does

    An instance of a FactCollector() isnt tied to a AnsibleModule
    instance, but the collect() method can be, so optionally pass
    in module to FactCollector.collect() (everywhere)

- add a default_collectors for list of default collectors

  import and use it from setup.py module

  eventually, would like to replace this with a plugin loader
  style class finder/loader

- unit tests for module_utils/facts/__init__.py
- add unit tests for ohai facts collector
- remove self.facts side effect on populate() in hardware/sunos.py
- convert OpenBSDHardware() to rm side effects on self.facts
- try to rm some self.facts side effects in Network()

    plumb in collected_facts from populate() where it is needed.

    stop passing collected_facts into Network() [via cached_facts=,
    where it eventually becomes self.facts]

- nothing provides Fact() cached_facts arg now, rm it

    Facts() should be internal only implementation so nothing
    should be using it.

    Of course, now someone will.

- add a Collector.name attr to build a map of name->_fact_ids

    To properly exclude a gather_subset spec like '!hardware', we
    need to know that 'hardware' also means 'devices', 'dmi', etc.
    Before, '!hardware' would remove the 'hardware' collector name
    but not 'devices'. Since both would end up in id_collector_map,
    we would still end up with the HardwareCollector in the collector
    list. End result being that '!hardware' wouldn't stop hardware
    from being collected.

    So we need to be able to build that map, so add the Collector.name
    attribute that is the primary name (like 'hardware') and let
    Collector._fact_ids be the other fact ids that a collector is
    responsible for.

    Construct the aliases_map of Collector.name -> set of _fact_ids
    in fact/__init__.py get_collector_names, and use it when we are
    populating the exclude set.

- refactor of distribution.py

    make the big OS_FAMILY literal a little easier to read
    Also keys can now be any string instead of python literals

    99% sure the test for 'KDE Neon' was wrong
    I don't see how/where it should or could get 'Neon' instead
    of 'KDE Neon' as provided in os-release NAME=

    Use 'distribution' string for key to OS_MAP

    ie, we dont need to make it a valid python label anymore so dont.

    move _has_dist_file to module as _file_exists
    easier to mock without mucking with os.path

    mv platform.system() calls to within get_distribution_facts() instead
    of Distribution() init.

- remove _json compat module

    The code in here was to support:

      -a 'json' python module that was not the standard one included
      with python since 2.6.

      - potentially fallback to simplejson if 'json' was not available.

    'json' is available for all supported python versions now so
    no longer needed.

- mv get_collector_names -> facts.collector
- mv collector_classes_from_gather_subset -> facts.collector
- mv collector tests from test_facts -> test_collector

- Use six's reduce() in sunos/netbsd hardware facts

- rm extraneous get_uname_version in utils

  only system/distribution.py uses it

- Remove Facts() subclass metaclass usage

  - using fact_id and a platform id for matching collectors

    gut most of Facts() subclasses

    rm Facts() subclasses with weird metaclass

    only add collectors that match the fact_ids and the platform_info
    to the list of collectors used.

    atm, a collectors platform_id will default to 'Generic', and
    any platform matches 'Generic'

    goal is to select collector classes including matching the
    systems platform in collector.py, instead of relying on
    metaclasses in hardware/*. To finish this, the various
    Facts() subclasses will need to be replaced entirely with
    Collector() subclasses.

    use collector classmethod platform_match() to match the platform

    This lets the particular class decide if it is compatible with
    a given platform_info. platform_info is a dict like obj, so it could be
    expanded in the future.

    Add a default platform_match to BaseFactCollector that matches
    platform_info['system'] == cls._platform

    They were needed previously to trigger a module
    load on all the collector classes when we import
    facts/hardare so that the Hardware() and related
    classes that used __new__ and find_all_subclasses()
    would work.

    Now that is done in collectors based on platform matching
    at runtime we dont need to do it py module import/parse
    time. So the non empty __init__.pys are no longer needed
    and their is a more flexible mechanism for selection
    platform specific stuff.

    facts/facts.py is no longer used, rm'ed

- if we dont find an implement class for gather spec.. just ignore it.

  Would be useful to add a warn to warn about this case.

- Fix SD-UX typo (should be HP-UX)

- Port fix for #21893 (0 sockets) to this branch

    This readds the change from 8ad182059d
    that got lost in merge/rebase

    Fixes #21893

- port sunos fact locale fix for #24542 to this branch

    based on e558ec19cd

    Fixes #24542

    Solaris fact fix (#24793)

    ensure locale for solaris fact gathering

    fixes issue with locale interfering with proper reading of decimals

- raise exceptions in the air like we just dont care.

    Pretty much ignore any not exit exception in facts
    collection. And add some test cases.

- added new selinux fact to clarify python lib

    the selinux fact is boolean false when the library is not installed,
    a dictionary/hash otherwise, but this is ambigous
    added new fact so we can eventually remove the type dichtomy and normalize it as a dict

    Re-add of devel commit 85c7a7b844 to
    the new code layout, since it got removed in merge/rebase
pull/23504/merge
Adrian Likins 8 years ago committed by GitHub
parent 3ca006ecbb
commit 45a9f96774

@ -20,7 +20,8 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from multiprocessing import Lock
from ansible.module_utils.facts import Facts
from ansible.module_utils.facts.system.pkg_mgr import PKG_MGRS
if 'action_write_locks' not in globals():
# Do not initialize this more than once because it seems to bash
@ -36,7 +37,8 @@ if 'action_write_locks' not in globals():
# These plugins are called directly by action plugins (not going through
# a strategy). We precreate them here as an optimization
mods = set(p['name'] for p in Facts.PKG_MGRS)
mods = set(p['name'] for p in PKG_MGRS)
mods.update(('copy', 'file', 'setup', 'slurp', 'stat'))
for mod_name in mods:
action_write_locks[mod_name] = Lock()

File diff suppressed because it is too large Load Diff

@ -0,0 +1,252 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from collections import defaultdict
import platform
from ansible.module_utils.facts import timeout
class BaseFactCollector:
_fact_ids = set()
_platform = 'Generic'
name = None
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()
for subset in gather_subset:
subset_id = subset
if subset_id == 'all':
additional_subsets.update(valid_subsets)
continue
if subset_id.startswith('!'):
subset = subset[1:]
if subset == 'all':
exclude_subsets.update(valid_subsets)
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))))
additional_subsets.add(subset)
if not additional_subsets:
additional_subsets.update(valid_subsets)
additional_subsets.difference_update(exclude_subsets)
additional_subsets.update(minimal_gather_subset)
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 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)
# TODO: can be a set()
seen_collector_classes = []
selected_collector_classes = []
for collector_name in collector_names:
collector_classes = all_fact_subsets.get(collector_name, [])
# TODO? log/warn if we dont find an implementation of a fact_id?
for collector_class in collector_classes:
if collector_class not in seen_collector_classes:
selected_collector_classes.append(collector_class)
seen_collector_classes.append(collector_class)
return selected_collector_classes

@ -0,0 +1,129 @@
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.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.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.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.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.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.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
# TODO: make config driven
collectors = [ApparmorFactCollector,
CmdLineFactCollector,
DateTimeFactCollector,
DistributionFactCollector,
DnsFactCollector,
EnvFactCollector,
FipsFactCollector,
HardwareCollector,
AIXHardwareCollector,
DarwinHardwareCollector,
DragonFlyHardwareCollector,
FreeBSDHardwareCollector,
HPUXHardwareCollector,
HurdHardwareCollector,
LinuxHardwareCollector,
NetBSDHardwareCollector,
OpenBSDHardwareCollector,
SunOSHardwareCollector,
LocalFactCollector,
LSBFactCollector,
NetworkCollector,
AIXNetworkCollector,
DarwinNetworkCollector,
DragonFlyNetworkCollector,
FreeBSDNetworkCollector,
HPUXNetworkCollector,
HurdNetworkCollector,
LinuxNetworkCollector,
NetBSDNetworkCollector,
OpenBSDNetworkCollector,
SunOSNetworkCollector,
PkgMgrFactCollector,
PlatformFactCollector,
PythonFactCollector,
SelinuxFactCollector,
ServiceMgrFactCollector,
SshPubKeyFactCollector,
SystemCapabilitiesFactCollector,
UserFactCollector,
VirtualCollector,
DragonFlyVirtualCollector,
FreeBSDVirtualCollector,
LinuxVirtualCollector,
OpenBSDVirtualCollector,
NetBSDVirtualCollector,
SunOSVirtualCollector,
HPUXVirtualCollector]
external_collectors = [FacterFactCollector,
OhaiFactCollector]

@ -0,0 +1,208 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
from ansible.module_utils.facts.hardware.base import Hardware, HardwareCollector
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_cores
- processor_count
"""
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()
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)
return hardware_facts
def get_cpu_facts(self):
cpu_facts = {}
cpu_facts['processor'] = []
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_count'] = int(i)
rc, out, err = self.module.run_command("/usr/sbin/lsattr -El " + cpudev + " -a type")
data = out.split(' ')
cpu_facts['processor'] = data[1]
rc, out, err = self.module.run_command("/usr/sbin/lsattr -El " + cpudev + " -a smt_threads")
data = out.split(' ')
cpu_facts['processor_cores'] = int(data[1])
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 | %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'] = []
# 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_facts['mounts'].append({'mount': fields[1],
'device': fields[0],
'fstype': fields[2],
'options': fields[6],
'time': '%s %s %s' % (fields[3], fields[4], fields[5])})
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_facts['mounts'].append({'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])})
return mount_facts
class AIXHardwareCollector(HardwareCollector):
_platform = 'AIX'
_fact_class = AIXHardware

@ -0,0 +1,53 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.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'])
_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

@ -0,0 +1,99 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
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
"""
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()
hardware_facts.update(mac_facts)
hardware_facts.update(cpu_facts)
hardware_facts.update(memory_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'] = 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']
return cpu_facts
def get_memory_facts(self):
memory_facts = {}
memory_facts['memtotal_mb'] = int(self.sysctl['hw.memsize']) // 1024 // 1024
rc, out, err = self.module.run_command("sysctl hw.usermem")
if rc == 0:
memory_facts['memfree_mb'] = int(out.splitlines()[-1].split()[1]) // 1024 // 1024
return memory_facts
class DarwinHardwareCollector(HardwareCollector):
_fact_class = DarwinHardware
_platform = 'Darwin'

@ -0,0 +1,26 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.hardware.base import HardwareCollector
from ansible.module_utils.facts.hardware.freebsd import FreeBSDHardware
class DragonFlyHardwareCollector(HardwareCollector):
# Note: This uses the freebsd fact class, there is no dragonfly hardware fact class
_fact_class = FreeBSDHardware
_platform = 'DragonFly'

@ -0,0 +1,195 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import json
import re
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
"""
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()
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(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'] = []
rc, out, err = self.module.run_command("/sbin/sysctl -n hw.ncpu")
cpu_facts['processor_count'] = out.strip()
dmesg_boot = get_file_content(FreeBSDHardware.DMESG_BOOT)
if not dmesg_boot:
rc, dmesg_boot, err = self.module.run_command("/sbin/dmesg")
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 = {}
rc, out, err = self.module.run_command("/sbin/sysctl vm.stats")
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
# 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/swapinfo -k")
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
@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()
size_total, size_available = get_mount_size(fields[1])
mount_facts['mounts'].append({
'mount': fields[1],
'device': fields[0],
'fstype': fields[2],
'options': fields[3],
'size_total': size_total,
'size_available': size_available
})
return mount_facts
def get_device_facts(self):
device_facts = {}
sysdir = '/dev'
device_facts['devices'] = {}
drives = re.compile('(ada?\d+|da\d+|a?cd\d+)') # TODO: rc, disks, err = self.module.run_command("/sbin/sysctl kern.disks")
slices = re.compile('(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 = dict(
bios_date='bios-release-date',
bios_version='bios-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'

@ -0,0 +1,161 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import re
from ansible.module_utils.facts.hardware.base import Hardware, HardwareCollector
class HPUXHardware(Hardware):
"""
HP-UX-specific subclass of Hardware. Defines memory and CPU facts:
- memfree_mb
- memtotal_mb
- swapfree_mb
- swaptotal_mb
- processor
- processor_cores
- processor_count
- model
- firmware
"""
platform = 'HP-UX'
def populate(self, collected_facts=None):
hardware_facts = {}
cpu_facts = self.get_cpu_facts(collected_facts=collected_facts)
memory_facts = self.get_memory_facts()
hw_facts = self.get_hw_facts()
hardware_facts.update(cpu_facts)
hardware_facts.update(memory_facts)
hardware_facts.update(hw_facts)
return hardware_facts
def get_cpu_facts(self, collected_facts=None):
cpu_facts = {}
collected_facts = collected_facts or {}
if collected_facts.get('ansible_architecture') == '9000/800':
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)
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)
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') == '9000/800':
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('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'

@ -0,0 +1,53 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.timeout import TimeoutError
from ansible.module_utils.facts.hardware.base import HardwareCollector
from ansible.module_utils.facts.hardware.linux import LinuxHardware
class HurdHardware(LinuxHardware):
"""
GNU Hurd specific subclass of Hardware. Define memory and mount facts
based on procfs compatibility translator mimicking the interface of
the Linux kernel.
"""
platform = 'GNU'
def populate(self, collected_facts=None):
hardware_facts = {}
uptime_facts = self.get_uptime_facts()
memory_facts = self.get_memory_facts()
mount_facts = {}
try:
mount_facts = self.get_mount_facts()
except TimeoutError:
pass
hardware_facts.update(uptime_facts)
hardware_facts.update(memory_facts)
hardware_facts.update(mount_facts)
return hardware_facts
class HurdHardwareCollector(HardwareCollector):
_fact_class = HurdHardware
_platform = 'GNU'

@ -0,0 +1,663 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import errno
import json
import os
import re
import sys
from ansible.module_utils.basic 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
# import this as a module to ensure we get the same module isntance
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.*"')
def populate(self, collected_facts=None):
hardware_facts = {}
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:
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(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
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()
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 data:
xen_paravirt = True
# model name is for Intel arch, Processor (mind the uppercase P)
# works for some ARM devices, like the Sheevaplug.
if key in ['model name', 'Processor', 'vendor_id', 'cpu', 'Vendor']:
if 'processor' not in cpu_facts:
cpu_facts['processor'] = []
cpu_facts['processor'].append(data[1].strip())
if key == 'vendor_id':
vendor_id_occurrence += 1
if key == 'model name':
model_name_occurrence += 1
i += 1
elif key == 'physical id':
physid = data[1].strip()
if physid not in sockets:
sockets[physid] = 1
elif key == 'core id':
coreid = data[1].strip()
if coreid not in sockets:
cores[coreid] = 1
elif key == 'cpu cores':
sockets[physid] = int(data[1].strip())
elif key == 'siblings':
cores[coreid] = int(data[1].strip())
elif key == '# processors':
cpu_facts['processor_cores'] = int(data[1].strip())
# 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
# 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'])
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 -- http://www.dmtf.org/sites/default/files/standards/documents/DSP0134_2.7.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"]
DMI_DICT = {
'bios_date': '/sys/devices/virtual/dmi/id/bios_date',
'bios_version': '/sys/devices/virtual/dmi/id/bios_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_version': 'bios-version',
'form_factor': 'chassis-type',
'product_name': 'system-product-name',
'product_serial': 'system-serial-number',
'product_uuid': 'system-uuid',
'product_version': 'system-version',
'system_vendor': 'system-manufacturer'
}
for (k, v) in DMI_DICT.items():
if dmi_bin is not None:
(rc, out, err) = self.module.run_command('%s -s %s' % (dmi_bin, v))
if rc == 0:
# Strip out commented lines (specific dmidecode output)
thisvalue = ''.join([line for line in out.splitlines() if not line.startswith('#')])
try:
json.dumps(thisvalue)
except UnicodeDecodeError:
thisvalue = "NA"
dmi_facts[k] = thisvalue
else:
dmi_facts[k] = 'NA'
else:
dmi_facts[k] = 'NA'
return dmi_facts
def _run_lsblk(self, lsblk_path):
# call lsblk and collect all uuids
# --exclude 2 makes lsblk ignore floppy disks, which are slower to answer than typical timeouts
# this uses the linux major device number
# for details see https://www.kernel.org/doc/Documentation/devices.txt
args = ['--list', '--noheadings', '--paths', '--output', 'NAME,UUID', '--exclude', '2']
cmd = [lsblk_path] + args
rc, out, err = self.module.run_command(cmd)
return rc, out, err
def _lsblk_uuid(self):
uuids = {}
lsblk_path = self.module.get_bin_path("lsblk")
if not lsblk_path:
return uuids
rc, out, err = self._run_lsblk(lsblk_path)
if rc != 0:
return uuids
# each line will be in format:
# <devicename><some whitespace><uuid>
# /dev/sda1 32caaec3-ef40-4691-a3b6-438c3f9bc1c0
for lsblk_line in out.splitlines():
if not lsblk_line:
continue
line = lsblk_line.strip()
fields = line.rsplit(None, 1)
if len(fields) < 2:
continue
device_name, uuid = fields[0].strip(), fields[1].strip()
if device_name in uuids:
continue
uuids[device_name] = uuid
return uuids
def _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
@timeout.timeout()
def get_mount_facts(self):
mount_facts = {}
mount_facts['mounts'] = []
bind_mounts = self._find_bind_mounts()
uuids = self._lsblk_uuid()
mtab_entries = self._mtab_entries()
mounts = []
for fields in mtab_entries:
device, mount, fstype, options = fields[0], fields[1], fields[2], fields[3]
if not device.startswith('/') and ':/' not in device:
continue
if fstype == 'none':
continue
size_total, size_available = get_mount_size(mount)
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):
options += ",bind"
mount_info = {'mount': mount,
'device': device,
'fstype': fstype,
'options': options,
# statvfs data
'size_total': size_total,
'size_available': size_available,
'uuid': uuids.get(device, 'N/A')}
mounts.append(mount_info)
mount_facts['mounts'] = mounts
return mount_facts
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_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:]
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
if "virtual" in path:
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
if virtual:
continue
d = {}
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')
if sg_inq:
device = "/dev/%s" % (block)
rc, drivedata, err = self.module.run_command([sg_inq, device])
if rc == 0:
serial = re.search("Unit serial number:\s+(\w+)", drivedata)
if serial:
d['serial'] = serial.group(1)
for key in ['vendor', 'model']:
d[key] = get_file_content(sysdir + "/device/" + key)
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 + "\d+)", folder)
if m:
part = {}
partname = m.group(1)
part_sysdir = sysdir + "/" + 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']) * float(part['sectorsize'])))
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(".*?(\[(.*)\])", 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']) * float(d['sectorsize']))
d['host'] = ""
# domains are numbered (0 to ffff), bus (0 to ff), slot (0 to 1f), and function (0 to 7).
m = re.match(".+/([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 + "\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 = {}
if os.getuid() == 0 and self.module.get_bin_path('vgs'):
lvm_util_options = '--noheadings --nosuffix --units g'
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.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.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.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

@ -0,0 +1,164 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import re
from ansible.module_utils.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
"""
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()
hardware_facts.update(cpu_facts)
hardware_facts.update(memory_facts)
hardware_facts.update(mount_facts)
hardware_facts.update(dmi_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()
size_total, size_available = get_mount_size(fields[1])
mount_facts['mounts'].append({
'mount': fields[1],
'device': fields[0],
'fstype': fields[2],
'options': fields[3],
'size_total': size_total,
'size_available': size_available
})
return mount_facts
def get_dmi_facts(self):
dmi_facts = {}
# We don't use dmidecode(1) here because:
# - it would add dependency on an external package
# - dmidecode(1) 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
class NetBSDHardwareCollector(HardwareCollector):
_fact_class = NetBSDHardware
_platform = 'NetBSD'

@ -0,0 +1,172 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
from ansible.module_utils._text import to_text
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
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'])
# TODO: change name
cpu_facts = self.get_processor_facts()
memory_facts = self.get_memory_facts()
device_facts = self.get_device_facts()
dmi_facts = self.get_dmi_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(mount_facts)
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
size_total, size_available = get_mount_size(fields[1])
mount_facts['mounts'].append({
'mount': fields[1],
'device': fields[0],
'fstype': fields[2],
'options': fields[3],
'size_total': size_total,
'size_available': size_available
})
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_processor_facts(self):
cpu_facts = {}
processor = []
for i in range(int(self.sysctl['hw.ncpu'])):
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.ncpu']
cpu_facts['processor_cores'] = self.sysctl['hw.ncpu']
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(1) here because:
# - it would add dependency on an external package
# - dmidecode(1) 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'

@ -0,0 +1,267 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
from ansible.module_utils.six.moves import reduce
from ansible.module_utils.basic 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
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)
self.module.run_command_environ_update = {'LANG': 'C', 'LC_ALL': 'C', 'LC_NUMERIC': 'C'}
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')
size_total, size_available = get_mount_size(fields[1])
mount_facts['mounts'].append({
'mount': fields[1],
'device': fields[0],
'fstype': fields[2],
'options': fields[3],
'time': fields[4],
'size_total': size_total,
'size_available': size_available
})
return mount_facts
def get_dmi_facts(self):
dmi_facts = {}
uname_path = self.module.get_bin_path("prtdiag")
rc, out, err = self.module.run_command(uname_path)
"""
rc returns 1
"""
if out:
system_conf = out.split('\n')[0]
found = re.search(r'(\w+\sEnterprise\s\w+)', system_conf)
if found:
dmi_facts['product_name'] = found.group(1)
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 = {}
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 = {}
# On Solaris, unix:0:system_misc:snaptime is created shortly after machine boots up
# and displays tiem in seconds. This is much easier than using uptime as we would
# need to have a parsing procedure for translating from human-readable to machine-readable
# format.
# Example output:
# unix:0:system_misc:snaptime 1175.410463590
rc, out, err = self.module.run_command('/usr/bin/kstat -p unix:0:system_misc:snaptime')
if rc != 0:
return
uptime_facts['uptime_seconds'] = int(float(out.split('\t')[1]))
return uptime_facts
class SunOSHardwareCollector(HardwareCollector):
_fact_class = SunOSHardware
_platform = 'SunOS'

@ -0,0 +1,39 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
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)

@ -0,0 +1,144 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
from ansible.module_utils.facts.network.base import NetworkCollector
from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
class AIXNetwork(GenericBsdIfconfigNetwork):
"""
This is the AIX Network Class.
It uses the GenericBsdIfconfigNetwork unchanged.
"""
platform = 'AIX'
def get_default_interfaces(self, route_path):
netstat_path = self.module.get_bin_path('netstat')
rc, out, err = self.module.run_command([netstat_path, '-nr'])
interface = dict(v4={}, v6={})
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('^\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'

@ -0,0 +1,70 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.collector import BaseFactCollector
class Network:
"""
This is a generic Network subclass of Facts. This should be further
subclassed to implement per platform. If you subclass this,
you must define:
- interfaces (a list of interface names)
- interface_<name> dictionary of ipv4, ipv6, and mac address information.
All subclasses MUST define platform.
"""
platform = 'Generic'
# FIXME: remove load_on_init when we can
def __init__(self, module, load_on_init=False):
self.module = module
# TODO: more or less abstract/NotImplemented
def populate(self, collected_facts=None):
return {}
class NetworkCollector(BaseFactCollector):
# MAYBE: we could try to build this based on the arch specific implemementation of Network() or its kin
name = 'network'
_fact_class = Network
_fact_ids = set(['interfaces',
'default_ipv4',
'default_ipv6',
'all_ipv4_addresses',
'all_ipv6_addresses'])
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

@ -0,0 +1,49 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.network.base import NetworkCollector
from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
class DarwinNetwork(GenericBsdIfconfigNetwork):
"""
This is the Mac OS X/Darwin Network Class.
It uses the GenericBsdIfconfigNetwork unchanged
"""
platform = 'Darwin'
# media line is different to the default FreeBSD one
def parse_media_line(self, words, current_if, ips):
# not sure if this is useful - we also drop information
current_if['media'] = 'Unknown' # Mac does not give us this
current_if['media_select'] = words[1]
if len(words) > 2:
# MacOSX sets the media to '<unknown type>' for bridge interface
# and parsing splits this into two words; this if/else helps
if words[1] == '<unknown' and words[2] == 'type>':
current_if['media_select'] = 'Unknown'
current_if['media_type'] = 'unknown type'
else:
current_if['media_type'] = words[2][1:-1]
if len(words) > 3:
current_if['media_options'] = self.get_options(words[3])
class DarwinNetworkCollector(NetworkCollector):
_fact_class = DarwinNetwork
_platform = 'Darwin'

@ -0,0 +1,33 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.network.base import NetworkCollector
from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
class DragonFlyNetwork(GenericBsdIfconfigNetwork):
"""
This is the DragonFly Network Class.
It uses the GenericBsdIfconfigNetwork unchanged.
"""
platform = 'DragonFly'
class DragonFlyNetworkCollector(NetworkCollector):
_fact_class = DragonFlyNetwork
_platform = 'DragonFly'

@ -0,0 +1,33 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.network.base import NetworkCollector
from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
class FreeBSDNetwork(GenericBsdIfconfigNetwork):
"""
This is the FreeBSD Network Class.
It uses the GenericBsdIfconfigNetwork unchanged.
"""
platform = 'FreeBSD'
class FreeBSDNetworkCollector(NetworkCollector):
_fact_class = FreeBSDNetwork
_platform = 'FreeBSD'

@ -0,0 +1,266 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
import socket
import struct
from ansible.module_utils.facts.network.base import Network
class GenericBsdIfconfigNetwork(Network):
"""
This is a generic BSD subclass of Network using the ifconfig command.
It defines
- interfaces (a list of interface names)
- interface_<name> dictionary of ipv4, ipv6, and mac address information.
- all_ipv4_addresses and all_ipv6_addresses: lists of all configured addresses.
"""
platform = 'Generic_BSD_Ifconfig'
def populate(self, collected_facts=None):
network_facts = {}
ifconfig_path = self.module.get_bin_path('ifconfig')
if ifconfig_path is None:
return network_facts
route_path = self.module.get_bin_path('route')
if route_path is None:
return network_facts
default_ipv4, default_ipv6 = self.get_default_interfaces(route_path)
interfaces, ips = self.get_interfaces_info(ifconfig_path)
interfaces = self.detect_type_media(interfaces)
self.merge_default_interface(default_ipv4, interfaces, 'ipv4')
self.merge_default_interface(default_ipv6, interfaces, 'ipv6')
network_facts['interfaces'] = 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 8.8.8.8 -> Google public DNS
# route -n get -inet6 2404:6800:400a:800::1012 -> ipv6.google.com
# to find out the default outgoing interface, address, and gateway
command = dict(v4=[route_path, '-n', 'get', '8.8.8.8'],
v6=[route_path, '-n', 'get', '-inet6', '2404:6800:400a:800::1012'])
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.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]
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 OS X 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('^\S', line) and len(words) > 3:
current_if = self.parse_interface_line(words)
interfaces[current_if['device']] = current_if
elif words[0].startswith('options='):
self.parse_options_line(words, current_if, ips)
elif words[0] == 'nd6':
self.parse_nd6_line(words, current_if, ips)
elif words[0] == 'ether':
self.parse_ether_line(words, current_if, ips)
elif words[0] == 'media:':
self.parse_media_line(words, current_if, ips)
elif words[0] == 'status:':
self.parse_status_line(words, current_if, ips)
elif words[0] == 'lladdr':
self.parse_lladdr_line(words, current_if, ips)
elif words[0] == 'inet':
self.parse_inet_line(words, current_if, ips)
elif words[0] == 'inet6':
self.parse_inet6_line(words, current_if, ips)
elif words[0] == 'tunnel':
self.parse_tunnel_line(words, current_if, ips)
else:
self.parse_unknown_line(words, current_if, ips)
return interfaces, ips
def parse_interface_line(self, words):
device = words[0][0:-1]
current_if = {'device': device, 'ipv4': [], 'ipv6': [], 'type': 'unknown'}
current_if['flags'] = self.get_options(words[1])
if 'LOOPBACK' in current_if['flags']:
current_if['type'] = 'loopback'
current_if['macaddress'] = 'unknown' # will be overwritten later
if len(words) >= 5: # Newer FreeBSD versions
current_if['metric'] = words[3]
current_if['mtu'] = words[5]
else:
current_if['mtu'] = words[3]
return current_if
def parse_options_line(self, words, current_if, ips):
# Mac has options like this...
current_if['options'] = self.get_options(words[0])
def parse_nd6_line(self, words, current_if, ips):
# FreeBSD has options like this...
current_if['options'] = self.get_options(words[1])
def parse_ether_line(self, words, current_if, ips):
current_if['macaddress'] = words[1]
current_if['type'] = 'ether'
def parse_media_line(self, words, current_if, ips):
# not sure if this is useful - we also drop information
current_if['media'] = words[1]
if len(words) > 2:
current_if['media_select'] = words[2]
if len(words) > 3:
current_if['media_type'] = words[3][1:]
if len(words) > 4:
current_if['media_options'] = self.get_options(words[4])
def parse_status_line(self, words, current_if, ips):
current_if['status'] = words[1]
def parse_lladdr_line(self, words, current_if, ips):
current_if['lladdr'] = words[1]
def parse_inet_line(self, words, current_if, ips):
# netbsd show aliases like this
# lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 33184
# inet 127.0.0.1 netmask 0xff000000
# inet alias 127.1.1.1 netmask 0xff000000
if words[1] == 'alias':
del words[1]
address = {'address': words[1]}
# deal with hex netmask
if re.match('([0-9a-f]){8}', words[3]) and len(words[3]) == 8:
words[3] = '0x' + words[3]
if words[3].startswith('0x'):
address['netmask'] = socket.inet_ntoa(struct.pack('!L', int(words[3], base=16)))
else:
# otherwise assume this is a dotted quad
address['netmask'] = words[3]
# 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))
# broadcast may be given or we need to calculate
if len(words) > 5:
address['broadcast'] = words[5]
else:
address['broadcast'] = socket.inet_ntoa(struct.pack('!L', address_bin | (~netmask_bin & 0xffffffff)))
# 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]}
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]
if len(ifinfo[ip_type]) > 0:
for item in ifinfo[ip_type][0]:
defaults[item] = ifinfo[ip_type][0][item]

@ -0,0 +1,82 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.network.base import Network, NetworkCollector
class HPUXNetwork(Network):
"""
HP-UX-specifig subclass of Network. Defines networking facts:
- default_interface
- interfaces (a list of interface names)
- interface_<name> dictionary of ipv4 address information.
"""
platform = 'HP-UX'
def populate(self, collected_facts=None):
network_facts = {}
netstat_path = self.module.get_bin_path('netstat')
if netstat_path is None:
return network_facts
default_interfaces_facts = self.get_default_interfaces()
network_facts.update(default_interfaces_facts)
interfaces = self.get_interfaces_info()
network_facts['interfaces'] = interfaces.keys()
for iface in interfaces:
network_facts[iface] = interfaces[iface]
return network_facts
def get_default_interfaces(self):
default_interfaces = {}
rc, out, err = self.module.run_command("/usr/bin/netstat -nr")
lines = out.splitlines()
for line in lines:
words = line.split()
if len(words) > 1:
if words[0] == 'default':
default_interfaces['default_interface'] = words[4]
default_interfaces['default_gateway'] = words[1]
return default_interfaces
def get_interfaces_info(self):
interfaces = {}
rc, out, err = self.module.run_command("/usr/bin/netstat -ni")
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'

@ -0,0 +1,86 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
from ansible.module_utils.facts.network.base import Network, NetworkCollector
class HurdPfinetNetwork(Network):
"""
This is a GNU Hurd specific subclass of Network. It use fsysopts to
get the ip address and support only pfinet.
"""
platform = 'GNU'
_socket_dir = '/servers/socket/'
def 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
# FIXME: extract to method
# FIXME: exit early on falsey socket_path and un-indent whole block
if 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
class HurdNetworkCollector(NetworkCollector):
_platform = 'GNU'
_fact_class = HurdPfinetNetwork

@ -0,0 +1,311 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import glob
import os
import re
import socket
import struct
from ansible.module_utils.facts.network.base import Network, NetworkCollector
from ansible.module_utils.facts.utils import get_file_content
class LinuxNetwork(Network):
"""
This is a Linux-specific subclass of Network. It defines
- interfaces (a list of interface names)
- interface_<name> dictionary of ipv4, ipv6, and mac address information.
- all_ipv4_addresses and all_ipv6_addresses: lists of all configured addresses.
- ipv4_address and ipv6_address: the first non-local address for each family.
"""
platform = 'Linux'
INTERFACE_TYPE = {
'1': 'ether',
'32': 'infiniband',
'512': 'ppp',
'772': 'loopback',
'65534': 'tunnel',
}
def populate(self, collected_facts=None):
network_facts = {}
ip_path = self.module.get_bin_path('ip')
if ip_path is None:
return network_facts
default_ipv4, default_ipv6 = self.get_default_interfaces(ip_path,
collected_facts=collected_facts)
interfaces, ips = self.get_interfaces_info(ip_path, default_ipv4, default_ipv6)
network_facts['interfaces'] = interfaces.keys()
for iface in interfaces:
network_facts[iface] = interfaces[iface]
network_facts['default_ipv4'] = default_ipv4
network_facts['default_ipv6'] = default_ipv6
network_facts['all_ipv4_addresses'] = ips['all_ipv4_addresses']
network_facts['all_ipv6_addresses'] = ips['all_ipv6_addresses']
return network_facts
def get_default_interfaces(self, ip_path, collected_facts=None):
collected_facts = collected_facts or {}
# Use the commands:
# ip -4 route get 8.8.8.8 -> Google public DNS
# ip -6 route get 2404:6800:400a:800::1012 -> ipv6.google.com
# to find out the default outgoing interface, address, and gateway
command = dict(
v4=[ip_path, '-4', 'route', 'get', '8.8.8.8'],
v6=[ip_path, '-6', 'route', 'get', '2404:6800:400a:800::1012']
)
interface = dict(v4={}, v6={})
for v in 'v4', 'v6':
if (v == 'v6' and collected_facts.get('ansible_os_family') == 'RedHat' and
collected_facts.get('ansible_distribution_version', '').startswith('4.')):
continue
if v == 'v6' and not socket.has_ipv6:
continue
rc, out, err = self.module.run_command(command[v], errors='surrogate_then_replace')
if not out:
# v6 routing may result in
# RTNETLINK answers: Invalid argument
continue
words = out.splitlines()[0].split()
# A valid output starts with the queried address on the first line
if len(words) > 0 and words[0] == command[v][-1]:
for i in range(len(words) - 1):
if words[i] == 'dev':
interface[v]['interface'] = words[i + 1]
elif words[i] == 'src':
interface[v]['address'] = words[i + 1]
elif words[i] == 'via' and words[i + 1] != command[v][-1]:
interface[v]['gateway'] = words[i + 1]
return interface['v4'], interface['v6']
def get_interfaces_info(self, ip_path, default_ipv4, default_ipv6):
interfaces = {}
ips = dict(
all_ipv4_addresses=[],
all_ipv6_addresses=[],
)
# FIXME: maybe split into smaller methods?
# FIXME: this is pretty much a constructor
for path in glob.glob('/sys/class/net/*'):
if not os.path.isdir(path):
continue
device = os.path.basename(path)
interfaces[device] = {'device': device}
if os.path.exists(os.path.join(path, 'address')):
macaddress = get_file_content(os.path.join(path, 'address'), default='')
if macaddress and macaddress != '00:00:00:00:00:00':
interfaces[device]['macaddress'] = macaddress
if os.path.exists(os.path.join(path, 'mtu')):
interfaces[device]['mtu'] = int(get_file_content(os.path.join(path, 'mtu')))
if os.path.exists(os.path.join(path, 'operstate')):
interfaces[device]['active'] = get_file_content(os.path.join(path, 'operstate')) != 'down'
if os.path.exists(os.path.join(path, 'device', 'driver', 'module')):
interfaces[device]['module'] = os.path.basename(os.path.realpath(os.path.join(path, 'device', 'driver', 'module')))
if os.path.exists(os.path.join(path, 'type')):
_type = get_file_content(os.path.join(path, 'type'))
interfaces[device]['type'] = self.INTERFACE_TYPE.get(_type, 'unknown')
if os.path.exists(os.path.join(path, 'bridge')):
interfaces[device]['type'] = 'bridge'
interfaces[device]['interfaces'] = [os.path.basename(b) for b in glob.glob(os.path.join(path, 'brif', '*'))]
if os.path.exists(os.path.join(path, 'bridge', 'bridge_id')):
interfaces[device]['id'] = get_file_content(os.path.join(path, 'bridge', 'bridge_id'), default='')
if os.path.exists(os.path.join(path, 'bridge', 'stp_state')):
interfaces[device]['stp'] = get_file_content(os.path.join(path, 'bridge', 'stp_state')) == '1'
if os.path.exists(os.path.join(path, 'bonding')):
interfaces[device]['type'] = 'bonding'
interfaces[device]['slaves'] = get_file_content(os.path.join(path, 'bonding', 'slaves'), default='').split()
interfaces[device]['mode'] = get_file_content(os.path.join(path, 'bonding', 'mode'), default='').split()[0]
interfaces[device]['miimon'] = get_file_content(os.path.join(path, 'bonding', 'miimon'), default='').split()[0]
interfaces[device]['lacp_rate'] = get_file_content(os.path.join(path, 'bonding', 'lacp_rate'), default='').split()[0]
primary = get_file_content(os.path.join(path, 'bonding', 'primary'))
if primary:
interfaces[device]['primary'] = primary
path = os.path.join(path, 'bonding', 'all_slaves_active')
if os.path.exists(path):
interfaces[device]['all_slaves_active'] = get_file_content(path) == '1'
if os.path.exists(os.path.join(path, 'bonding_slave')):
interfaces[device]['perm_macaddress'] = get_file_content(os.path.join(path, 'bonding_slave', 'perm_hwaddr'), default='')
if os.path.exists(os.path.join(path, 'device')):
interfaces[device]['pciid'] = os.path.basename(os.readlink(os.path.join(path, 'device')))
if os.path.exists(os.path.join(path, 'speed')):
speed = get_file_content(os.path.join(path, 'speed'))
if speed is not None:
interfaces[device]['speed'] = int(speed)
# Check whether an interface is in promiscuous mode
if os.path.exists(os.path.join(path, 'flags')):
promisc_mode = False
# The second byte indicates whether the interface is in promiscuous mode.
# 1 = promisc
# 0 = no promisc
data = int(get_file_content(os.path.join(path, 'flags')), 16)
promisc_mode = (data & 0x0100 > 0)
interfaces[device]['promisc'] = promisc_mode
# TODO: determine if this needs to be in a nested scope/closure
def parse_ip_output(output, secondary=False):
for line in output.splitlines():
if not line:
continue
words = line.split()
broadcast = ''
if words[0] == 'inet':
if '/' in words[1]:
address, netmask_length = words[1].split('/')
if len(words) > 3:
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}
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,
})
# add this secondary IP to the main device
if secondary:
if "ipv4_secondaries" not in interfaces[device]:
interfaces[device]["ipv4_secondaries"] = []
interfaces[device]["ipv4_secondaries"].append({
'address': address,
'broadcast': broadcast,
'netmask': netmask,
'network': network,
})
# 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
# NOTE: macadress 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', device]
rc, primary_data, stderr = self.module.run_command(args, errors='surrogate_then_replace')
args = [ip_path, 'addr', 'show', 'secondary', device]
rc, secondary_data, stderr = self.module.run_command(args, errors='surrogate_then_replace')
parse_ip_output(primary_data)
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('SOF_TIMESTAMPING_(\w+)', stdout)]
data['hw_timestamp_filters'] = [m.lower() for m in re.findall('HWTSTAMP_FILTER_(\w+)', stdout)]
m = re.search('PTP Hardware Clock: (\d+)', stdout)
if m:
data['phc_index'] = int(m.groups()[0])
return data
class LinuxNetworkCollector(NetworkCollector):
_platform = 'Linux'
_fact_class = LinuxNetwork

@ -0,0 +1,48 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.network.base import NetworkCollector
from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
class NetBSDNetwork(GenericBsdIfconfigNetwork):
"""
This is the NetBSD Network Class.
It uses the GenericBsdIfconfigNetwork
"""
platform = 'NetBSD'
def parse_media_line(self, words, current_if, ips):
# example of line:
# $ ifconfig
# ne0: flags=8863<UP,BROADCAST,NOTRAILERS,RUNNING,SIMPLEX,MULTICAST> mtu 1500
# ec_capabilities=1<VLAN_MTU>
# ec_enabled=0
# address: 00:20:91:45:00:78
# media: Ethernet 10baseT full-duplex
# inet 192.168.156.29 netmask 0xffffff00 broadcast 192.168.156.255
current_if['media'] = words[1]
if len(words) > 2:
current_if['media_type'] = words[2]
if len(words) > 3:
current_if['media_options'] = words[3].split(',')
class NetBSDNetworkCollector(NetworkCollector):
_fact_class = NetBSDNetwork
_platform = 'NetBSD'

@ -0,0 +1,42 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.network.base import NetworkCollector
from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
class OpenBSDNetwork(GenericBsdIfconfigNetwork):
"""
This is the OpenBSD Network Class.
It uses the GenericBsdIfconfigNetwork.
"""
platform = 'OpenBSD'
# OpenBSD 'ifconfig -a' does not have information about aliases
def get_interfaces_info(self, ifconfig_path, ifconfig_options='-aA'):
return super(OpenBSDNetwork, self).get_interfaces_info(ifconfig_path, ifconfig_options)
# Return macaddress instead of lladdr
def parse_lladdr_line(self, words, current_if, ips):
current_if['macaddress'] = words[1]
current_if['type'] = 'ether'
class OpenBSDNetworkCollector(NetworkCollector):
_fact_class = OpenBSDNetwork
_platform = 'OpenBSD'

@ -0,0 +1,116 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
from ansible.module_utils.facts.network.base import NetworkCollector
from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
class SunOSNetwork(GenericBsdIfconfigNetwork):
"""
This is the SunOS Network Class.
It uses the GenericBsdIfconfigNetwork.
Solaris can have different FLAGS and MTU for IPv4 and IPv6 on the same interface
so these facts have been moved inside the 'ipv4' and 'ipv6' lists.
"""
platform = 'SunOS'
# Solaris 'ifconfig -a' will print interfaces twice, once for IPv4 and again for IPv6.
# MTU and FLAGS also may differ between IPv4 and IPv6 on the same interface.
# 'parse_interface_line()' checks for previously seen interfaces before defining
# 'current_if' so that IPv6 facts don't clobber IPv4 facts (or vice versa).
def get_interfaces_info(self, ifconfig_path):
interfaces = {}
current_if = {}
ips = dict(
all_ipv4_addresses=[],
all_ipv6_addresses=[],
)
rc, out, err = self.module.run_command([ifconfig_path, '-a'])
for line in out.splitlines():
if line:
words = line.split()
if re.match('^\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'

@ -0,0 +1,85 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import json
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'])
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

@ -0,0 +1,72 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import json
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()
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

@ -0,0 +1,35 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
import re
def get_sysctl(module, prefixes):
sysctl_cmd = module.get_bin_path('sysctl')
cmd = [sysctl_cmd]
cmd.extend(prefixes)
rc, out, err = module.run_command(cmd)
if rc != 0:
return dict()
sysctl = dict()
for line in out.splitlines():
if not line:
continue
(key, value) = re.split('\s?=\s?|: ', line, maxsplit=1)
sysctl[key] = value.strip()
return sysctl

@ -0,0 +1,39 @@
# Collect facts related to apparmor
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
from ansible.module_utils.facts.collector import BaseFactCollector
class ApparmorFactCollector(BaseFactCollector):
name = 'apparmor'
_fact_ids = set()
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

@ -0,0 +1,55 @@
# Collect facts related to systems 'capabilities' via capsh
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.collector import BaseFactCollector
class SystemCapabilitiesFactCollector(BaseFactCollector):
name = 'caps'
_fact_ids = set(['system_capabilities',
'system_capabilities_enforced'])
def collect(self, module=None, collected_facts=None):
facts_dict = {}
if not module:
return facts_dict
capsh_path = module.get_bin_path('capsh')
# NOTE: early exit 'if not crash_path' and unindent rest of method -akl
if capsh_path:
# NOTE: -> get_caps_data()/parse_caps_data() for easier mocking -akl
rc, out, err = module.run_command([capsh_path, "--print"], errors='surrogate_then_replace')
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

@ -0,0 +1,50 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import shlex
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()
def collect(self, module=None, collected_facts=None):
cmdline_facts = {}
data = get_file_content('/proc/cmdline')
if not data:
return cmdline_facts
cmdline_facts['cmdline'] = {}
try:
for piece in shlex.split(data):
item = piece.split('=', 1)
if len(item) == 1:
cmdline_facts['cmdline'][item[0]] = True
else:
cmdline_facts['cmdline'][item[0]] = item[1]
except ValueError:
pass
return cmdline_facts

@ -0,0 +1,59 @@
# Data and time related facts collection for ansible.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import datetime
import time
from ansible.module_utils.facts.collector import BaseFactCollector
class DateTimeFactCollector(BaseFactCollector):
name = 'date_time'
_fact_ids = set()
def collect(self, module=None, collected_facts=None):
facts_dict = {}
date_time_facts = {}
now = datetime.datetime.now()
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')
if date_time_facts['epoch'] == '' or date_time_facts['epoch'][0] == '%':
# NOTE: in this case, the epoch wont match the rest of the date_time facts? ie, it's a few milliseconds later..? -akl
date_time_facts['epoch'] = str(int(time.time()))
date_time_facts['date'] = now.strftime('%Y-%m-%d')
date_time_facts['time'] = now.strftime('%H:%M:%S')
date_time_facts['iso8601_micro'] = now.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ")
date_time_facts['iso8601'] = now.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_offset'] = time.strftime("%z")
facts_dict['date_time'] = date_time_facts
return facts_dict

@ -0,0 +1,579 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import platform
import re
from ansible.module_utils.facts.utils import get_file_content
from ansible.module_utils.facts.collector import BaseFactCollector
def get_uname_version(module):
rc, out, err = module.run_command(['uname', '-v'])
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
OSDIST_LIST = (
{'path': '/etc/oracle-release', 'name': 'OracleLinux'},
{'path': '/etc/slackware-version', 'name': 'Slackware'},
{'path': '/etc/redhat-release', 'name': 'RedHat'},
{'path': '/etc/vmware-release', 'name': 'VMwareESX', 'allowempty': True},
{'path': '/etc/openwrt_release', 'name': 'OpenWrt'},
{'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': 'SuSE'},
{'path': '/etc/SuSE-release', 'name': 'SuSE'},
{'path': '/etc/gentoo-release', 'name': 'Gentoo'},
{'path': '/etc/os-release', 'name': 'Debian'},
{'path': '/etc/lsb-release', 'name': 'Mandriva'},
{'path': '/etc/altlinux-release', 'name': 'Altlinux'},
{'path': '/etc/sourcemage-release', 'name': 'SMGL'},
{'path': '/etc/os-release', 'name': 'NA'},
{'path': '/etc/coreos/update.conf', 'name': 'Coreos'},
{'path': '/usr/lib/os-release', 'name': 'ClearLinux'},
)
SEARCH_STRING = {
'OracleLinux': 'Oracle Linux',
'RedHat': 'Red Hat',
'Altlinux': 'ALT Linux',
'ClearLinux': 'Clear Linux Software for Intel Architecture',
'SMGL': 'Source Mage GNU/Linux',
}
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 = {}
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 platform.dist()
if self.SEARCH_STRING[name] in dist_file_content:
# this sets distribution=RedHat if 'Red Hat' shows up in data
# self.facts['distribution'] = name
dist_file_dict['distribution'] = name
else:
# this sets distribution to what's in the data, e.g. CentOS, Scientific, ...
# self.facts['distribution'] = dist_file_content.split()[0]
dist_file_dict['distribution'] = dist_file_content.split()[0]
return True, 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
# print('distfunc_name: %s' % distfunc_name)
distfunc = getattr(self, distfunc_name)
# print('distfunc: %s' % distfunc)
parsed, dist_file_dict = distfunc(name, dist_file_content, path, collected_facts)
return parsed, dist_file_dict
except AttributeError as exc:
print('exc: %s' % exc)
# this should never happen, but if it does fail quitely 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 = platform.dist()
distribution_guess = {}
distribution_guess['distribution'] = dist[0].capitalize() or 'NA'
distribution_guess['distribution_version'] = dist[1] or 'NA'
distribution_guess['distribution_major_version'] = dist[1].split('.')[0] or 'NA'
distribution_guess['distribution_release'] = dist[2] 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)
if not has_dist_file:
# keep looking
continue
# first valid os dist file we find we count
# FIXME: coreos and a few other bits expect this
# self.facts['distribution'] = name
dist_file_facts['distribution'] = name
dist_file_facts['distribution_file_variety'] = name
dist_file_facts['distribution_file_path'] = path
parsed_dist_file, parsed_dist_file_facts = self._parse_dist_file(name, dist_file_content, path, dist_file_facts)
dist_file_facts['distribution_file_parsed'] = parsed_dist_file
# finally found the right os dist file and were able to parse it
if parsed_dist_file:
dist_file_facts.update(parsed_dist_file_facts)
break
return dist_file_facts
# distribution_facts.update(dist_file_facts)
# return distribution_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('\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 # TODO: remove # huh?
return False, amazon_facts # TODO: remove
amazon_facts['distribution'] = 'Amazon'
amazon_facts['distribution_version'] = data.split()[-1]
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('^VERSION_ID="?([0-9]+\.?[0-9]*)"?', line)
if distribution_version:
suse_facts['distribution_version'] = distribution_version.group(1)
if 'open' in data.lower():
release = re.search('^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('^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)
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("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()
elif 'Ubuntu' in data:
debian_facts['distribution'] = 'Ubuntu'
# nothing else to do, Ubuntu gets correct info from python functions
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 collected_facts['distribution'] == '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
dist = platform.dist()
distro = dist[0]
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
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.
"""
# every distribution name mentioned here, must have one of
# - allowempty == True
# - be listed in SEARCH_STRING
# - have a function get_distribution_DISTNAME implemented
OSDIST_LIST = (
{'path': '/etc/oracle-release', 'name': 'OracleLinux'},
{'path': '/etc/slackware-version', 'name': 'Slackware'},
{'path': '/etc/redhat-release', 'name': 'RedHat'},
{'path': '/etc/vmware-release', 'name': 'VMwareESX', 'allowempty': True},
{'path': '/etc/openwrt_release', 'name': 'OpenWrt'},
{'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': 'SuSE'},
{'path': '/etc/SuSE-release', 'name': 'SuSE'},
{'path': '/etc/gentoo-release', 'name': 'Gentoo'},
{'path': '/etc/os-release', 'name': 'Debian'},
{'path': '/etc/lsb-release', 'name': 'Mandriva'},
{'path': '/etc/altlinux-release', 'name': 'Altlinux'},
{'path': '/etc/sourcemage-release', 'name': 'SMGL'},
{'path': '/etc/os-release', 'name': 'NA'},
{'path': '/etc/coreos/update.conf', 'name': 'Coreos'},
{'path': '/usr/lib/os-release', 'name': 'ClearLinux'},
)
SEARCH_STRING = {
'OracleLinux': 'Oracle Linux',
'RedHat': 'Red Hat',
'Altlinux': 'ALT Linux',
'ClearLinux': 'Clear Linux Software for Intel Architecture',
'SMGL': 'Source Mage GNU/Linux',
}
OS_FAMILY_MAP = {'RedHat': ['RedHat', 'Fedora', 'CentOS', 'Scientific', 'SLC',
'Ascendos', 'CloudLinux', 'PSBM', 'OracleLinux', 'OVS',
'OEL', 'Amazon', 'Virtuozzo', 'XenServer'],
'Debian': ['Debian', 'Ubuntu', 'Raspbian', 'Neon', 'KDE neon'],
'Suse': ['SuSE', 'SLES', 'SLED', 'openSUSE', 'openSUSE Tumbleweed',
'SLES_SAP', 'SUSE_LINUX', 'openSUSE Leap'],
'Archlinux': ['Archlinux', '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']}
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_version'] = data[0]
aix_facts['distribution_release'] = data[1]
return aix_facts
def get_distribution_HPUX(self):
hpux_facts = {}
rc, out, err = self.module.run_command("/usr/sbin/swlist |egrep 'HPUX.*OE.*[AB].[0-9]+\.[0-9]+'", use_unsafe_shell=True)
data = re.search('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]
darwin_facts['distribution_version'] = data
return darwin_facts
def get_distribution_FreeBSD(self):
freebsd_facts = {}
freebsd_facts['distribution_release'] = platform.release()
data = re.search('(\d+)\.(\d+)-RELEASE.*', freebsd_facts['distribution_release'])
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('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):
return {}
def get_distribution_NetBSD(self):
netbsd_facts = {}
# FIXME: poking at self.facts, should eventually make these each a collector
platform_release = platform.release()
netbsd_facts['distribution_major_version'] = platform_release.split('.')[0]
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 = {}
# print('platform.release: %s' % distribution_release)
data = get_file_content('/etc/release').splitlines()[0]
# print('get_file_content: data=%s' % data)
if 'Solaris' in data:
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
return sunos_facts
uname_v = get_uname_version(self.module)
distribution_version = None
# print('uname_v: %s' % uname_v)
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')
# print('sunos_facts: %s' % sunos_facts)
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'])
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

@ -0,0 +1,67 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.utils import get_file_content
from ansible.module_utils.facts.collector import BaseFactCollector
class DnsFactCollector(BaseFactCollector):
name = 'dns'
_fact_ids = set()
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

@ -0,0 +1,37 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
from ansible.module_utils.six import iteritems
from ansible.module_utils.facts.collector import BaseFactCollector
class EnvFactCollector(BaseFactCollector):
name = 'env'
_fact_ids = set()
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

@ -0,0 +1,37 @@
# Determine if a system is in 'fips' mode
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
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()
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

@ -0,0 +1,90 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import glob
import json
import os
import stat
from ansible.module_utils.six.moves import configparser
from ansible.module_utils.six.moves import StringIO
from ansible.module_utils.facts.utils import get_file_content
from ansible.module_utils.facts.collector import BaseFactCollector
class LocalFactCollector(BaseFactCollector):
name = 'local'
_fact_ids = set()
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 = {}
for fn in sorted(glob.glob(fact_path + '/*.fact')):
# where it will sit under local facts
fact_base = os.path.basename(fn).replace('.fact', '')
if stat.S_IXUSR & os.stat(fn)[stat.ST_MODE]:
# run it
# try to read it as json first
# if that fails read it with ConfigParser
# if that fails, skip it
try:
rc, out, err = module.run_command(fn)
except UnicodeError:
fact = 'error loading fact - output of running %s was not utf-8' % fn
local[fact_base] = fact
local_facts['local'] = local
return local_facts
else:
out = get_file_content(fn, default='')
# load raw json
fact = 'loading %s' % fact_base
try:
fact = json.loads(out)
except ValueError:
# load raw ini
cp = configparser.ConfigParser()
try:
cp.readfp(StringIO(out))
except configparser.Error:
fact = "error loading fact - please check content"
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
local[fact_base] = fact
local_facts['local'] = local
return local_facts

@ -0,0 +1,101 @@
# Collect facts related to LSB (Linux Standard Base)
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
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()
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]
facts_dict['lsb'] = lsb_facts
return facts_dict

@ -0,0 +1,73 @@
# Collect facts related to the system package manager
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
from ansible.module_utils.facts.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/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': '/sbin/apk', 'name': 'apk'},
{'path': '/usr/sbin/pkg', 'name': 'pkgng'},
{'path': '/usr/sbin/swlist', 'name': 'HP-UX'},
{'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'},
]
# the fact ends up being 'pkg_mgr' so stick with that naming/spelling
class PkgMgrFactCollector(BaseFactCollector):
name = 'pkg_mgr'
_fact_ids = set()
def collect(self, module=None, collected_facts=None):
facts_dict = {}
collected_facts = collected_facts or {}
pkg_mgr_name = None
if collected_facts.get('system') == 'OpenBSD':
facts_dict['pkg_mgr'] = 'openbsd_pkg'
return facts_dict
pkg_mgr_name = 'unknown'
for pkg in PKG_MGRS:
if os.path.exists(pkg['path']):
pkg_mgr_name = pkg['name']
facts_dict['pkg_mgr'] = pkg_mgr_name
return facts_dict

@ -0,0 +1,94 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
import socket
import platform
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',
'machine',
'python_version',
'machine_id'])
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['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

@ -0,0 +1,60 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import sys
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()
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

@ -0,0 +1,86 @@
# Collect facts related to selinux
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.collector import BaseFactCollector
try:
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()
def collect(self, module=None, collected_facts=None):
facts_dict = {}
selinux_facts = {}
# This is weird. The value of the facts 'selinux' key can be False or a dict
if not HAVE_SELINUX:
facts_dict['selinux'] = False
facts_dict['selinux_python_present'] = False
return facts_dict
facts_dict['selinux_python_present'] = True
if not selinux.is_selinux_enabled():
selinux_facts['status'] = 'disabled'
# NOTE: this could just return in the above clause and the rest of this is up an indent -akl
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

@ -0,0 +1,138 @@
# Collect facts related to system service manager and init.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import platform
import re
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 distutils.version import LooseVersion
class ServiceMgrFactCollector(BaseFactCollector):
name = 'service_mgr'
_fact_ids = set()
def is_systemd_managed(self, 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
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:
# FIXME: return code isnt checked
# FIXME: if stdout is empty string, odd things
# FIXME: other code seems to think we could get proc_1 == None past this point
rc, proc_1, err = module.run_command("ps -p 1 -o comm|tail -n 1", use_unsafe_shell=True)
# If 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 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
# FIXME: empty string proc_1 staus empty string
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
# FIXME: empty string still falls through
service_mgr_name = proc_1_map.get(proc_1, proc_1)
# FIXME: replace with a system->service_mgr_name map?
# start with the easy ones
elif collected_facts.get('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('system', '') or collected_facts.get('system') in ['Bitrig', 'DragonFly']:
# FIXME: we might want to break out to individual BSDs or 'rc'
service_mgr_name = 'bsdinit'
elif collected_facts.get('system') == 'AIX':
service_mgr_name = 'src'
elif collected_facts.get('system') == 'SunOS':
service_mgr_name = 'smf'
elif collected_facts.get('distribution') == 'OpenWrt':
service_mgr_name = 'openwrt_init'
elif collected_facts.get('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 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

@ -0,0 +1,52 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.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'])
def collect(self, module=None, collected_facts=None):
ssh_pub_key_facts = {}
keytypes = ('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 type_ in keytypes:
factname = 'ssh_host_key_%s_public' % type_
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, type_)
keydata = get_file_content(key_filename)
if keydata is not None:
ssh_pub_key_facts[factname] = keydata.split()[1]
return ssh_pub_key_facts

@ -0,0 +1,50 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import getpass
import os
import pwd
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'])
def collect(self, module=None, collected_facts=None):
user_facts = {}
user_facts['user_id'] = getpass.getuser()
pwent = pwd.getpwnam(getpass.getuser())
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

@ -0,0 +1,66 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import signal
# 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"):
def decorator(func):
def _handle_timeout(signum, frame):
msg = 'Timer expired after %s seconds' % globals().get('GATHER_TIMEOUT')
raise TimeoutError(msg)
def wrapper(*args, **kwargs):
local_seconds = seconds
if local_seconds is None:
local_seconds = globals().get('GATHER_TIMEOUT') or DEFAULT_GATHER_TIMEOUT
signal.signal(signal.SIGALRM, _handle_timeout)
signal.alarm(local_seconds)
try:
result = func(*args, **kwargs)
finally:
signal.alarm(0)
return result
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

@ -0,0 +1,59 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
import os
def get_file_content(path, default=None, strip=True):
data = default
if os.path.exists(path) and os.access(path, os.R_OK):
try:
try:
datafile = open(path)
data = datafile.read()
if strip:
data = data.strip()
if len(data) == 0:
data = default
finally:
datafile.close()
except:
# ignore errors as some jails/containers might have readable permissions but not allow reads to proc
# done in 2 blocks for 2.4 compat
pass
return data
def get_file_lines(path):
'''get list of lines from file'''
data = get_file_content(path)
if data:
ret = data.splitlines()
else:
ret = []
return ret
def get_mount_size(mountpoint):
size_total = None
size_available = None
try:
statvfs_result = os.statvfs(mountpoint)
size_total = statvfs_result.f_frsize * statvfs_result.f_blocks
size_available = statvfs_result.f_frsize * (statvfs_result.f_bavail)
except OSError:
pass
return size_total, size_available

@ -0,0 +1,70 @@
# base classes for virtualization facts
# -*- coding: utf-8 -*-
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
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': ''}
return virtual_facts
class VirtualCollector(BaseFactCollector):
name = 'virtual'
_fact_class = Virtual
_fact_ids = set(['virtualization_type',
'virtualization_role'])
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

@ -0,0 +1,25 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.facts.virtual.freebsd import FreeBSDVirtual, VirtualCollector
class DragonFlyVirtualCollector(VirtualCollector):
# Note the _fact_class impl is actually the FreeBSDVirtual impl
_fact_class = FreeBSDVirtual
_platform = 'DragonFly'

@ -0,0 +1,47 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
from ansible.module_utils.facts.virtual.base import Virtual, VirtualCollector
class FreeBSDVirtual(Virtual):
"""
This is a FreeBSD-specific subclass of Virtual. It defines
- virtualization_type
- virtualization_role
"""
platform = 'FreeBSD'
def get_virtual_facts(self):
virtual_facts = {}
# Set empty values as default
virtual_facts['virtualization_type'] = ''
virtual_facts['virtualization_role'] = ''
if os.path.exists('/dev/xen/xenstore'):
virtual_facts['virtualization_type'] = 'xen'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
class FreeBSDVirtualCollector(VirtualCollector):
_fact_class = FreeBSDVirtual
_platform = 'FreeBSD'

@ -0,0 +1,62 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import 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 = {}
if os.path.exists('/usr/sbin/vecheck'):
rc, out, err = self.module.run_command("/usr/sbin/vecheck")
if rc == 0:
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):
virtual_facts['virtualization_type'] = 'guest'
virtual_facts['virtualization_role'] = 'HPVM vPar'
elif rc == 0 and re.match('.*Running.*HPVM guest.*', out):
virtual_facts['virtualization_type'] = 'guest'
virtual_facts['virtualization_role'] = 'HPVM IVM'
elif rc == 0 and re.match('.*Running.*HPVM host.*', out):
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:
virtual_facts['virtualization_type'] = 'guest'
virtual_facts['virtualization_role'] = 'HP nPar'
return virtual_facts
class HPUXVirtualCollector(VirtualCollector):
_fact_class = HPUXVirtual
_platform = 'HP-UX'

@ -0,0 +1,228 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import glob
import os
import re
from ansible.module_utils.facts.virtual.base import Virtual, VirtualCollector
from ansible.module_utils.facts.utils import get_file_content, get_file_lines
class LinuxVirtual(Virtual):
"""
This is a Linux-specific subclass of Virtual. It defines
- virtualization_type
- virtualization_role
"""
platform = 'Linux'
# For more information, check: http://people.redhat.com/~rjones/virt-what/
def get_virtual_facts(self):
virtual_facts = {}
# 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):
virtual_facts['virtualization_type'] = 'docker'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
if re.search('/lxc/', line) or re.search('/machine.slice/machine-lxc', line):
virtual_facts['virtualization_type'] = 'lxc'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
# 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'):
if re.search('container=lxc', line):
virtual_facts['virtualization_type'] = 'lxc'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
if os.path.exists('/proc/vz'):
virtual_facts['virtualization_type'] = 'openvz'
if os.path.exists('/proc/bc'):
virtual_facts['virtualization_role'] = 'host'
else:
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
systemd_container = get_file_content('/run/systemd/container')
if systemd_container:
virtual_facts['virtualization_type'] = systemd_container
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
if os.path.exists("/proc/xen"):
virtual_facts['virtualization_type'] = 'xen'
virtual_facts['virtualization_role'] = 'guest'
try:
for line in get_file_lines('/proc/xen/capabilities'):
if "control_d" in line:
virtual_facts['virtualization_role'] = 'host'
except IOError:
pass
return virtual_facts
product_name = get_file_content('/sys/devices/virtual/dmi/id/product_name')
if product_name in ['KVM', 'Bochs']:
virtual_facts['virtualization_type'] = 'kvm'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
if product_name == 'RHEV Hypervisor':
virtual_facts['virtualization_type'] = 'RHEV'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
if product_name == 'VMware Virtual Platform':
virtual_facts['virtualization_type'] = 'VMware'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
if product_name == 'OpenStack Nova':
virtual_facts['virtualization_type'] = 'openstack'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
bios_vendor = get_file_content('/sys/devices/virtual/dmi/id/bios_vendor')
if bios_vendor == 'Xen':
virtual_facts['virtualization_type'] = 'xen'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
if bios_vendor == 'innotek GmbH':
virtual_facts['virtualization_type'] = 'virtualbox'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
sys_vendor = get_file_content('/sys/devices/virtual/dmi/id/sys_vendor')
# FIXME: This does also match hyperv
if sys_vendor == 'Microsoft Corporation':
virtual_facts['virtualization_type'] = 'VirtualPC'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
if sys_vendor == 'Parallels Software International Inc.':
virtual_facts['virtualization_type'] = 'parallels'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
if sys_vendor == 'QEMU':
virtual_facts['virtualization_type'] = 'kvm'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
if sys_vendor == 'oVirt':
virtual_facts['virtualization_type'] = 'kvm'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
if sys_vendor == 'OpenStack Foundation':
virtual_facts['virtualization_type'] = 'openstack'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
if os.path.exists('/proc/self/status'):
for line in get_file_lines('/proc/self/status'):
if re.match('^VxID: \d+', line):
virtual_facts['virtualization_type'] = 'linux_vserver'
if re.match('^VxID: 0', line):
virtual_facts['virtualization_role'] = 'host'
else:
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
if os.path.exists('/proc/cpuinfo'):
for line in get_file_lines('/proc/cpuinfo'):
if re.match('^model name.*QEMU Virtual CPU', line):
virtual_facts['virtualization_type'] = 'kvm'
elif re.match('^vendor_id.*User Mode Linux', line):
virtual_facts['virtualization_type'] = 'uml'
elif re.match('^model name.*UML', line):
virtual_facts['virtualization_type'] = 'uml'
elif re.match('^vendor_id.*PowerVM Lx86', line):
virtual_facts['virtualization_type'] = 'powervm_lx86'
elif re.match('^vendor_id.*IBM/S390', line):
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':
virtual_facts['virtualization_type'] = data[1].strip()
else:
virtual_facts['virtualization_type'] = 'ibm_systemz'
else:
continue
if virtual_facts['virtualization_type'] == 'PR/SM':
virtual_facts['virtualization_role'] = 'LPAR'
else:
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
# 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:
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:
if open(f).read().rstrip() == 'vdsm':
virtual_facts['virtualization_type'] = 'RHEV'
break
except:
pass
else:
virtual_facts['virtualization_type'] = 'kvm'
else:
virtual_facts['virtualization_type'] = 'kvm'
virtual_facts['virtualization_role'] = 'host'
return virtual_facts
if 'vboxdrv' in modules:
virtual_facts['virtualization_type'] = 'virtualbox'
virtual_facts['virtualization_role'] = 'host'
return virtual_facts
# If none of the above matches, return 'NA' for virtualization_type
# and virtualization_role. This allows for proper grouping.
virtual_facts['virtualization_type'] = 'NA'
virtual_facts['virtualization_role'] = 'NA'
return virtual_facts
class LinuxVirtualCollector(VirtualCollector):
_fact_class = LinuxVirtual
_platform = 'Linux'

@ -0,0 +1,50 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
from ansible.module_utils.facts.virtual.base import Virtual, VirtualCollector
from ansible.module_utils.facts.virtual.sysctl import VirtualSysctlDetectionMixin
class NetBSDVirtual(Virtual, VirtualSysctlDetectionMixin):
platform = 'NetBSD'
def get_virtual_facts(self):
virtual_facts = {}
# Set empty values as default
virtual_facts['virtualization_type'] = ''
virtual_facts['virtualization_role'] = ''
virtual_product_facts = self.detect_virt_product('machdep.dmi.system-product')
virtual_facts.update(virtual_product_facts)
if virtual_facts['virtualization_type'] == '':
virtual_vendor_facts = self.detect_virt_vendor('machdep.dmi.system-vendor')
virtual_facts.update(virtual_vendor_facts)
if os.path.exists('/dev/xencons'):
virtual_facts['virtualization_type'] = 'xen'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
class NetBSDVirtualCollector(VirtualCollector):
_fact_class = NetBSDVirtual
_platform = 'NetBSD'

@ -0,0 +1,64 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
from ansible.module_utils.facts.virtual.base import Virtual, VirtualCollector
from ansible.module_utils.facts.virtual.sysctl import VirtualSysctlDetectionMixin
from ansible.module_utils.facts.utils import get_file_content
class OpenBSDVirtual(Virtual, VirtualSysctlDetectionMixin):
"""
This is a OpenBSD-specific subclass of Virtual. It defines
- virtualization_type
- virtualization_role
"""
platform = 'OpenBSD'
DMESG_BOOT = '/var/run/dmesg.boot'
def get_virtual_facts(self):
virtual_facts = {}
# Set empty values as default
virtual_facts['virtualization_type'] = ''
virtual_facts['virtualization_role'] = ''
virtual_product_facts = self.detect_virt_product('hw.product')
virtual_facts.update(virtual_product_facts)
if virtual_facts['virtualization_type'] == '':
virtual_vendor_facts = self.detect_virt_vendor('hw.vendor')
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:
virtual_facts['virtualization_type'] = 'vmm'
virtual_facts['virtualization_role'] = 'host'
return virtual_facts
class OpenBSDVirtualCollector(VirtualCollector):
_fact_class = OpenBSDVirtual
_platform = 'OpenBSD'

@ -0,0 +1,120 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
from ansible.module_utils.facts.virtual.base import Virtual, VirtualCollector
class SunOSVirtual(Virtual):
"""
This is a SunOS-specific subclass of Virtual. It defines
- virtualization_type
- virtualization_role
- container
"""
platform = 'SunOS'
def get_virtual_facts(self):
virtual_facts = {}
# 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 and out.rstrip() != "global":
virtual_facts['container'] = 'zone'
# Check if it's a branded zone (i.e. Solaris 8/9 zone)
if os.path.isdir('/.SUNWnative'):
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:
virtual_facts['virtualization_type'] = 'vmware'
virtual_facts['virtualization_role'] = 'guest'
if 'VirtualBox' in line:
virtual_facts['virtualization_type'] = 'virtualbox'
virtual_facts['virtualization_role'] = 'guest'
if os.path.exists('/proc/vz'):
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':
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:
virtual_facts['virtualization_type'] = 'vmware'
virtual_facts['virtualization_role'] = 'guest'
elif 'Parallels' in line:
virtual_facts['virtualization_type'] = 'parallels'
virtual_facts['virtualization_role'] = 'guest'
elif 'VirtualBox' in line:
virtual_facts['virtualization_type'] = 'virtualbox'
virtual_facts['virtualization_role'] = 'guest'
elif 'HVM domU' in line:
virtual_facts['virtualization_type'] = 'xen'
virtual_facts['virtualization_role'] = 'guest'
elif 'KVM' in line:
virtual_facts['virtualization_type'] = 'kvm'
virtual_facts['virtualization_role'] = 'guest'
return virtual_facts
class SunOSVirtualCollector(VirtualCollector):
_fact_class = SunOSVirtual
_platform = 'SunOS'

@ -0,0 +1,68 @@
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import 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 = {}
self.detect_sysctl()
# FIXME: exit early on falsey self.sysctl_path and unindent
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|Bochs|SmartDC).*', out):
virtual_product_facts['virtualization_type'] = 'kvm'
virtual_product_facts['virtualization_role'] = 'guest'
elif re.match('.*VMware.*', out):
virtual_product_facts['virtualization_type'] = 'VMware'
virtual_product_facts['virtualization_role'] = 'guest'
elif out.rstrip() == 'VirtualBox':
virtual_product_facts['virtualization_type'] = 'virtualbox'
virtual_product_facts['virtualization_role'] = 'guest'
elif out.rstrip() == 'HVM domU':
virtual_product_facts['virtualization_type'] = 'xen'
virtual_product_facts['virtualization_role'] = 'guest'
elif out.rstrip() == 'Parallels':
virtual_product_facts['virtualization_type'] = 'parallels'
virtual_product_facts['virtualization_role'] = 'guest'
elif out.rstrip() == 'RHEV Hypervisor':
virtual_product_facts['virtualization_type'] = 'RHEV'
virtual_product_facts['virtualization_role'] = 'guest'
return virtual_product_facts
def detect_virt_vendor(self, key):
virtual_vendor_facts = {}
self.detect_sysctl()
# FIXME: exit early on falsey self.sysctl_path and unindent
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':
virtual_vendor_facts['virtualization_type'] = 'kvm'
virtual_vendor_facts['virtualization_role'] = 'guest'
if out.rstrip() == 'OpenBSD':
virtual_vendor_facts['virtualization_type'] = 'vmm'
virtual_vendor_facts['virtualization_role'] = 'guest'
return virtual_vendor_facts

@ -116,24 +116,152 @@ EXAMPLES = """
# Display facts from Windows hosts with custom facts stored in C(C:\\custom_facts).
# ansible windows -m setup -a "fact_path='c:\\custom_facts'"
"""
import fnmatch
import sys
# import module snippets
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.facts import collector
from ansible.module_utils.facts.namespace import PrefixFactNamespace
from ansible.module_utils.facts import default_collectors
# This is the main entry point for setup.py facts.py.
# FIXME: This is coupled to AnsibleModule (it assumes module.params has keys 'gather_subset',
# 'gather_timeout', 'filter' instead of passing those are args or oblique ds
# module is passed in and self.module.misc_AnsibleModule_methods
# are used, so hard to decouple.
class AnsibleFactCollector(collector.BaseFactCollector):
'''A FactCollector that returns results under 'ansible_facts' top level key.
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 a filter_spec='' is equilv to filter_spec='*'
if not filter_spec or filter_spec == '*':
return facts_dict
return [(x, y) for x, y in facts_dict.items() if fnmatch.fnmatch(x, filter_spec)]
def collect(self, module=None, collected_facts=None):
collected_facts = collected_facts or {}
facts_dict = {}
facts_dict['ansible_facts'] = {}
for collector_obj in self.collectors:
info_dict = {}
# shallow copy of the accumulated collected facts to pass to each collector
# for reference.
collected_facts.update(facts_dict['ansible_facts'].copy())
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')
# filtered_info_dict = self._filter(info_dict, self.filter_spec)
# NOTE: If we want complicated fact dict merging, this is where it would hook in
facts_dict['ansible_facts'].update(self._filter(info_dict, self.filter_spec))
# TODO: this may be best place to apply fact 'filters' as well. They
# are currently ignored -akl
return facts_dict
class CollectorMetaDataCollector(collector.BaseFactCollector):
'''Collector that provides a facts with the gather_subset metadata.'''
name = 'gather_subset'
_fact_ids = set([])
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 main():
module = AnsibleModule(
argument_spec = dict(
argument_spec=dict(
gather_subset=dict(default=["all"], required=False, type='list'),
gather_timeout=dict(default=10, required=False, type='int'),
filter=dict(default="*", required=False),
fact_path=dict(default='/etc/ansible/facts.d', required=False, type='path'),
),
supports_check_mode = True,
supports_check_mode=True,
)
data = get_all_facts(module)
module.exit_json(**data)
# import module snippets
from ansible.module_utils.basic import *
from ansible.module_utils.facts import *
gather_subset = module.params['gather_subset']
gather_timeout = module.params['gather_timeout']
filter_spec = module.params['filter']
# TODO: this mimics existing behavior where gather_subset=["!all"] actually means
# to collect nothing except for the below list
# TODO: decide what '!all' means, I lean towards making it mean none, but likely needs
# some tweaking on how gather_subset operations are performed
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
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)
# print('collector_classes: %s' % pprint.pformat(collector_classes))
namespace = PrefixFactNamespace(namespace_name='ansible',
prefix='ansible_')
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)
# print('collectors: %s' % pprint.pformat(collectors))
fact_collector = \
AnsibleFactCollector(collectors=collectors,
filter_spec=filter_spec)
facts_dict = fact_collector.collect(module=module)
module.exit_json(**facts_dict)
if __name__ == '__main__':
main()

@ -5,7 +5,7 @@ testhost2 ansible_ssh_host=127.0.0.1 ansible_connection=local
testhost3 ansible_ssh_host=127.0.0.3
testhost4 ansible_ssh_host=127.0.0.4
# For testing fact gathering
facthost[0:9] ansible_host=1270.0.0.1 ansible_connection=local
facthost[0:20] ansible_host=1270.0.0.1 ansible_connection=local
[binary_modules]
testhost_binary_modules ansible_host=127.0.0.1 ansible_connection=local

@ -33,6 +33,8 @@
that:
- "'ansible_facts' in setup_result"
- "'ansible_local' in setup_result.ansible_facts"
- "'ansible_env' not in setup_result.ansible_facts"
- "'ansible_user_id' not in setup_result.ansible_facts"
- "'preferences' in setup_result.ansible_facts['ansible_local']"
- "'general' in setup_result.ansible_facts['ansible_local']['preferences']"
- "'bar' in setup_result.ansible_facts['ansible_local']['preferences']['general']"

@ -1,5 +1,21 @@
---
- hosts: facthost7
tags: [ 'fact_negation' ]
connection: local
gather_subset: "!hardware"
gather_facts: no
tasks:
- name: setup with not hardware
setup:
gather_subset:
- "!hardware"
register: not_hardware_facts
- name: debug setup with not hardware
debug:
var: not_hardware_facts
- hosts: facthost0
tags: [ 'fact_min' ]
connection: local
@ -16,6 +32,75 @@
- 'ansible_mounts|default("UNDEF_NET") != "UNDEF_HW"'
- 'ansible_virtualization_role|default("UNDEF_VIRT") != "UNDEF_VIRT"'
- hosts: facthost19
tags: [ 'fact_min' ]
connection: local
gather_facts: no
tasks:
- setup:
filter: "*env*"
register: facts_results
- name: Test that retrieving all facts filtered to env works
assert:
that:
- 'ansible_interfaces|default("UNDEF_NET") == "UNDEF_NET"'
- 'ansible_mounts|default("UNDEF_MOUNT") == "UNDEF_MOUNT"'
- 'ansible_virtualization_role|default("UNDEF_VIRT") == "UNDEF_VIRT"'
- 'ansible_env|default("UNDEF_ENV") != "UNDEF_ENV"'
- hosts: facthost13
tags: [ 'fact_min' ]
connection: local
gather_facts: no
tasks:
- setup:
filter: "ansible_user_id"
register: facts_results
- name: Test that retrieving all facts filtered to specific fact ansible_user_id works
assert:
that:
- 'ansible_user_id|default("UNDEF_USER") != "UNDEF_USER"'
- 'ansible_interfaces|default("UNDEF_NET") == "UNDEF_NET"'
- 'ansible_mounts|default("UNDEF_MOUNT") == "UNDEF_MOUNT"'
- 'ansible_virtualization_role|default("UNDEF_VIRT") == "UNDEF_VIRT"'
- 'ansible_env|default("UNDEF_ENV") == "UNDEF_ENV"'
- hosts: facthost11
tags: [ 'fact_min' ]
connection: local
gather_facts: no
tasks:
- setup:
filter: "*"
register: facts
- name: Test that retrieving all facts filtered to splat
assert:
that:
- 'ansible_user_id|default("UNDEF_MIN") != "UNDEF_MIN"'
- 'ansible_interfaces|default("UNDEF_NET") != "UNDEF_NET"'
- 'ansible_mounts|default("UNDEF_NET") != "UNDEF_HW"'
- 'ansible_virtualization_role|default("UNDEF_VIRT") != "UNDEF_VIRT"'
- hosts: facthost12
tags: [ 'fact_min' ]
connection: local
gather_facts: no
tasks:
- setup:
filter: ""
register: facts
- name: Test that retrieving all facts filtered to empty filter_spec works
assert:
that:
- 'ansible_user_id|default("UNDEF_MIN") != "UNDEF_MIN"'
- 'ansible_interfaces|default("UNDEF_NET") != "UNDEF_NET"'
- 'ansible_mounts|default("UNDEF_NET") != "UNDEF_HW"'
- 'ansible_virtualization_role|default("UNDEF_VIRT") != "UNDEF_VIRT"'
- hosts: facthost1
tags: [ 'fact_min' ]
connection: local
@ -102,6 +187,7 @@
- 'ansible_mounts|default("UNDEF_HW") == "UNDEF_HW"'
- 'ansible_virtualization_role|default("UNDEF_VIRT") != "UNDEF_VIRT"'
- hosts: facthost7
tags: [ 'fact_negation' ]
connection: local

@ -25,7 +25,6 @@ lib/ansible/module_utils/database.py
lib/ansible/module_utils/docker_common.py
lib/ansible/module_utils/ec2.py
lib/ansible/module_utils/f5_utils.py
lib/ansible/module_utils/facts.py
lib/ansible/module_utils/fortios.py
lib/ansible/module_utils/gcdns.py
lib/ansible/module_utils/gce.py
@ -624,7 +623,6 @@ lib/ansible/modules/system/sefcontext.py
lib/ansible/modules/system/selinux.py
lib/ansible/modules/system/seport.py
lib/ansible/modules/system/service.py
lib/ansible/modules/system/setup.py
lib/ansible/modules/system/solaris_zone.py
lib/ansible/modules/system/svc.py
lib/ansible/modules/system/sysctl.py

@ -0,0 +1,61 @@
# base unit test classes for ansible/module_utils/facts/ tests
# -*- coding: utf-8 -*-
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
# Make coding more python3-ish
from __future__ import (absolute_import, division)
__metaclass__ = type
from ansible.compat.tests import unittest
from ansible.compat.tests.mock import Mock
class BaseFactsTest(unittest.TestCase):
# just a base class, not an actual test
__test__ = False
gather_subset = ['all']
valid_subsets = None
fact_namespace = None
collector_class = None
# a dict ansible_facts. Some fact collectors depend on facts gathered by
# other collectors (like 'ansible_architecture' or 'ansible_system') which
# can be passed via the collected_facts arg to collect()
collected_facts = None
def _mock_module(self):
mock_module = Mock()
mock_module.params = {'gather_subset': self.gather_subset,
'gather_timeout': 5,
'filter': '*'}
mock_module.get_bin_path = Mock(return_value=None)
return mock_module
def test_collect(self):
module = self._mock_module()
fact_collector = self.collector_class()
facts_dict = fact_collector.collect(module=module, collected_facts=self.collected_facts)
self.assertIsInstance(facts_dict, dict)
return facts_dict
def test_collect_with_namespace(self):
module = self._mock_module()
fact_collector = self.collector_class()
facts_dict = fact_collector.collect_with_namespace(module=module,
collected_facts=self.collected_facts)
self.assertIsInstance(facts_dict, dict)
return facts_dict

@ -0,0 +1,228 @@
# unit tests for ansible other facter fact collector
# -*- coding: utf-8 -*-
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
# Make coding more python3-ish
from __future__ import (absolute_import, division)
__metaclass__ = type
from ansible.compat.tests.mock import Mock, patch
from .. base import BaseFactsTest
from ansible.module_utils.facts.other.facter import FacterFactCollector
facter_json_output = '''
{
"operatingsystemmajrelease": "25",
"hardwareisa": "x86_64",
"kernel": "Linux",
"path": "/home/testuser/src/ansible/bin:/home/testuser/src/ansible/test/runner:/home/testuser/perl5/bin:/home/testuser/perl5/bin:/home/testuser/bin:/home/testuser/.local/bin:/home/testuser/pythons/bin:/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/testuser/.cabal/bin:/home/testuser/gopath/bin:/home/testuser/.rvm/bin",
"memorysize": "15.36 GB",
"memoryfree": "4.88 GB",
"swapsize": "7.70 GB",
"swapfree": "6.75 GB",
"swapsize_mb": "7880.00",
"swapfree_mb": "6911.41",
"memorysize_mb": "15732.95",
"memoryfree_mb": "4997.68",
"lsbmajdistrelease": "25",
"macaddress": "02:42:ea:15:d8:84",
"id": "testuser",
"domain": "example.com",
"augeasversion": "1.7.0",
"os": {
"name": "Fedora",
"family": "RedHat",
"release": {
"major": "25",
"full": "25"
},
"lsb": {
"distcodename": "TwentyFive",
"distid": "Fedora",
"distdescription": "Fedora release 25 (Twenty Five)",
"release": ":core-4.1-amd64:core-4.1-noarch:cxx-4.1-amd64:cxx-4.1-noarch:desktop-4.1-amd64:desktop-4.1-noarch:languages-4.1-amd64:languages-4.1-noarch:printing-4.1-amd64:printing-4.1-noarch",
"distrelease": "25",
"majdistrelease": "25"
}
},
"processors": {
"models": [
"Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
"Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
"Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
"Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
"Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
"Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
"Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
"Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz"
],
"count": 8,
"physicalcount": 1
},
"architecture": "x86_64",
"hardwaremodel": "x86_64",
"operatingsystem": "Fedora",
"processor0": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
"processor1": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
"processor2": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
"processor3": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
"processor4": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
"processor5": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
"processor6": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
"processor7": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
"processorcount": 8,
"uptime_seconds": 1558090,
"fqdn": "myhostname.example.com",
"rubyversion": "2.3.3",
"gid": "testuser",
"physicalprocessorcount": 1,
"netmask": "255.255.0.0",
"uniqueid": "a8c01301",
"uptime_days": 18,
"interfaces": "docker0,em1,lo,vethf20ff12,virbr0,virbr1,virbr0_nic,virbr1_nic,wlp4s0",
"ipaddress_docker0": "172.17.0.1",
"macaddress_docker0": "02:42:ea:15:d8:84",
"netmask_docker0": "255.255.0.0",
"mtu_docker0": 1500,
"macaddress_em1": "3c:97:0e:e9:28:8e",
"mtu_em1": 1500,
"ipaddress_lo": "127.0.0.1",
"netmask_lo": "255.0.0.0",
"mtu_lo": 65536,
"macaddress_vethf20ff12": "ae:6e:2b:1e:a1:31",
"mtu_vethf20ff12": 1500,
"ipaddress_virbr0": "192.168.137.1",
"macaddress_virbr0": "52:54:00:ce:82:5e",
"netmask_virbr0": "255.255.255.0",
"mtu_virbr0": 1500,
"ipaddress_virbr1": "192.168.121.1",
"macaddress_virbr1": "52:54:00:b4:68:a9",
"netmask_virbr1": "255.255.255.0",
"mtu_virbr1": 1500,
"macaddress_virbr0_nic": "52:54:00:ce:82:5e",
"mtu_virbr0_nic": 1500,
"macaddress_virbr1_nic": "52:54:00:b4:68:a9",
"mtu_virbr1_nic": 1500,
"ipaddress_wlp4s0": "192.168.1.19",
"macaddress_wlp4s0": "5c:51:4f:e6:a8:e3",
"netmask_wlp4s0": "255.255.255.0",
"mtu_wlp4s0": 1500,
"virtual": "physical",
"is_virtual": false,
"partitions": {
"sda2": {
"size": "499091456"
},
"sda1": {
"uuid": "32caaec3-ef40-4691-a3b6-438c3f9bc1c0",
"size": "1024000",
"mount": "/boot"
}
},
"lsbdistcodename": "TwentyFive",
"lsbrelease": ":core-4.1-amd64:core-4.1-noarch:cxx-4.1-amd64:cxx-4.1-noarch:desktop-4.1-amd64:desktop-4.1-noarch:languages-4.1-amd64:languages-4.1-noarch:printing-4.1-amd64:printing-4.1-noarch", # noqa
"filesystems": "btrfs,ext2,ext3,ext4,xfs",
"system_uptime": {
"seconds": 1558090,
"hours": 432,
"days": 18,
"uptime": "18 days"
},
"ipaddress": "172.17.0.1",
"timezone": "EDT",
"ps": "ps -ef",
"rubyplatform": "x86_64-linux",
"rubysitedir": "/usr/local/share/ruby/site_ruby",
"uptime": "18 days",
"lsbdistrelease": "25",
"operatingsystemrelease": "25",
"facterversion": "2.4.3",
"kernelrelease": "4.9.14-200.fc25.x86_64",
"lsbdistdescription": "Fedora release 25 (Twenty Five)",
"network_docker0": "172.17.0.0",
"network_lo": "127.0.0.0",
"network_virbr0": "192.168.137.0",
"network_virbr1": "192.168.121.0",
"network_wlp4s0": "192.168.1.0",
"lsbdistid": "Fedora",
"selinux": true,
"selinux_enforced": false,
"selinux_policyversion": "30",
"selinux_current_mode": "permissive",
"selinux_config_mode": "permissive",
"selinux_config_policy": "targeted",
"hostname": "myhostname",
"osfamily": "RedHat",
"kernelmajversion": "4.9",
"blockdevice_sr0_size": 1073741312,
"blockdevice_sr0_vendor": "MATSHITA",
"blockdevice_sr0_model": "DVD-RAM UJ8E2",
"blockdevice_sda_size": 256060514304,
"blockdevice_sda_vendor": "ATA",
"blockdevice_sda_model": "SAMSUNG MZ7TD256",
"blockdevices": "sda,sr0",
"uptime_hours": 432,
"kernelversion": "4.9.14"
}
'''
class TestFacterCollector(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'facter']
valid_subsets = ['facter']
fact_namespace = 'ansible_facter'
collector_class = FacterFactCollector
def _mock_module(self):
mock_module = Mock()
mock_module.params = {'gather_subset': self.gather_subset,
'gather_timeout': 10,
'filter': '*'}
mock_module.get_bin_path = Mock(return_value='/not/actually/facter')
mock_module.run_command = Mock(return_value=(0, facter_json_output, ''))
return mock_module
@patch('ansible.module_utils.facts.other.facter.FacterFactCollector.get_facter_output')
def test_bogus_json(self, mock_get_facter_output):
module = self._mock_module()
# bogus json
mock_get_facter_output.return_value = '{'
fact_collector = self.collector_class()
facts_dict = fact_collector.collect(module=module)
self.assertIsInstance(facts_dict, dict)
self.assertEqual(facts_dict, {})
@patch('ansible.module_utils.facts.other.facter.FacterFactCollector.run_facter')
def test_facter_non_zero_return_code(self, mock_run_facter):
module = self._mock_module()
# bogus json
mock_run_facter.return_value = (1, '{}', '')
fact_collector = self.collector_class()
facts_dict = fact_collector.collect(module=module)
self.assertIsInstance(facts_dict, dict)
# This assumes no 'facter' entry at all is correct
self.assertNotIn('facter', facts_dict)
self.assertEqual(facts_dict, {})

File diff suppressed because it is too large Load Diff

@ -0,0 +1,108 @@
# unit tests for ansible system lsb fact collectors
# -*- coding: utf-8 -*-
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
# Make coding more python3-ish
from __future__ import (absolute_import, division)
__metaclass__ = type
from ansible.compat.tests.mock import Mock, patch
from .. base import BaseFactsTest
from ansible.module_utils.facts.system.lsb import LSBFactCollector
lsb_release_a_fedora_output = '''
LSB Version: :core-4.1-amd64:core-4.1-noarch:cxx-4.1-amd64:cxx-4.1-noarch:desktop-4.1-amd64:desktop-4.1-noarch:languages-4.1-amd64:languages-4.1-noarch:printing-4.1-amd64:printing-4.1-noarch
Distributor ID: Fedora
Description: Fedora release 25 (Twenty Five)
Release: 25
Codename: TwentyFive
''' # noqa
# FIXME: a
etc_lsb_release_ubuntu14 = '''DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.3 LTS"
'''
etc_lsb_release_no_decimal = '''DISTRIB_ID=AwesomeOS
DISTRIB_RELEASE=11
DISTRIB_CODENAME=stonehenge
DISTRIB_DESCRIPTION="AwesomeÖS 11"
'''
class TestLSBFacts(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'lsb']
valid_subsets = ['lsb']
fact_namespace = 'ansible_lsb'
collector_class = LSBFactCollector
def _mock_module(self):
mock_module = Mock()
mock_module.params = {'gather_subset': self.gather_subset,
'gather_timeout': 10,
'filter': '*'}
mock_module.get_bin_path = Mock(return_value='/usr/bin/lsb_release')
mock_module.run_command = Mock(return_value=(0, lsb_release_a_fedora_output, ''))
return mock_module
def test_lsb_release_bin(self):
module = self._mock_module()
fact_collector = self.collector_class()
facts_dict = fact_collector.collect(module=module)
self.assertIsInstance(facts_dict, dict)
self.assertEqual(facts_dict['lsb']['release'], '25')
self.assertEqual(facts_dict['lsb']['id'], 'Fedora')
self.assertEqual(facts_dict['lsb']['description'], 'Fedora release 25 (Twenty Five)')
self.assertEqual(facts_dict['lsb']['codename'], 'TwentyFive')
self.assertEqual(facts_dict['lsb']['major_release'], '25')
def test_etc_lsb_release(self):
module = self._mock_module()
module.get_bin_path = Mock(return_value=None)
with patch('ansible.module_utils.facts.system.lsb.os.path.exists',
return_value=True):
with patch('ansible.module_utils.facts.system.lsb.get_file_lines',
return_value=etc_lsb_release_ubuntu14.splitlines()):
fact_collector = self.collector_class()
facts_dict = fact_collector.collect(module=module)
self.assertIsInstance(facts_dict, dict)
self.assertEqual(facts_dict['lsb']['release'], '14.04')
self.assertEqual(facts_dict['lsb']['id'], 'Ubuntu')
self.assertEqual(facts_dict['lsb']['description'], '"Ubuntu 14.04.3 LTS"')
self.assertEqual(facts_dict['lsb']['codename'], 'trusty')
def test_etc_lsb_release_no_decimal_release(self):
module = self._mock_module()
module.get_bin_path = Mock(return_value=None)
with patch('ansible.module_utils.facts.system.lsb.os.path.exists',
return_value=True):
with patch('ansible.module_utils.facts.system.lsb.get_file_lines',
return_value=etc_lsb_release_no_decimal.splitlines()):
fact_collector = self.collector_class()
facts_dict = fact_collector.collect(module=module)
self.assertIsInstance(facts_dict, dict)
self.assertEqual(facts_dict['lsb']['release'], '11')
self.assertEqual(facts_dict['lsb']['id'], 'AwesomeOS')
self.assertEqual(facts_dict['lsb']['description'], '"AwesomeÖS 11"')
self.assertEqual(facts_dict['lsb']['codename'], 'stonehenge')

@ -0,0 +1,172 @@
# This file is part of Ansible
# -*- coding: utf-8 -*-
#
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
# Make coding more python3-ish
from __future__ import (absolute_import, division)
__metaclass__ = type
# for testing
from ansible.compat.tests import unittest
from ansible.module_utils.facts import collector
from ansible.module_utils.facts import default_collectors
class TestGetCollectorNames(unittest.TestCase):
def test_none(self):
res = collector.get_collector_names()
self.assertIsInstance(res, set)
self.assertEqual(res, set([]))
def test_empty_sets(self):
res = collector.get_collector_names(valid_subsets=frozenset([]),
minimal_gather_subset=frozenset([]),
gather_subset=set([]))
self.assertIsInstance(res, set)
self.assertEqual(res, set([]))
def test_empty_valid_and_min_with_all_gather_subset(self):
res = collector.get_collector_names(valid_subsets=frozenset([]),
minimal_gather_subset=frozenset([]),
gather_subset=set(['all']))
self.assertIsInstance(res, set)
self.assertEqual(res, set([]))
def test_one_valid_with_all_gather_subset(self):
valid_subsets = frozenset(['my_fact'])
res = collector.get_collector_names(valid_subsets=valid_subsets,
minimal_gather_subset=frozenset([]),
gather_subset=set(['all']))
self.assertIsInstance(res, set)
self.assertEqual(res, set(['my_fact']))
def test_one_minimal_with_all_gather_subset(self):
my_fact = 'my_fact'
valid_subsets = frozenset([my_fact])
minimal_gather_subset = valid_subsets
res = collector.get_collector_names(valid_subsets=valid_subsets,
minimal_gather_subset=minimal_gather_subset,
gather_subset=set(['all']))
self.assertIsInstance(res, set)
self.assertEqual(res, set(['my_fact']))
def test_with_all_gather_subset(self):
valid_subsets = frozenset(['my_fact', 'something_else', 'whatever'])
minimal_gather_subset = frozenset(['my_fact'])
# even with '!all', the minimal_gather_subset should be returned
res = collector.get_collector_names(valid_subsets=valid_subsets,
minimal_gather_subset=minimal_gather_subset,
gather_subset=set(['all']))
self.assertIsInstance(res, set)
self.assertEqual(res, set(['my_fact', 'something_else', 'whatever']))
def test_one_minimal_with_not_all_gather_subset(self):
valid_subsets = frozenset(['my_fact', 'something_else', 'whatever'])
minimal_gather_subset = frozenset(['my_fact'])
# even with '!all', the minimal_gather_subset should be returned
res = collector.get_collector_names(valid_subsets=valid_subsets,
minimal_gather_subset=minimal_gather_subset,
gather_subset=set(['!all']))
self.assertIsInstance(res, set)
self.assertEqual(res, set(['my_fact']))
def test_gather_subset_excludes(self):
valid_subsets = frozenset(['my_fact', 'something_else', 'whatever'])
minimal_gather_subset = frozenset(['my_fact'])
# even with '!all', the minimal_gather_subset should be returned
res = collector.get_collector_names(valid_subsets=valid_subsets,
minimal_gather_subset=minimal_gather_subset,
gather_subset=set(['all', '!my_fact', '!whatever']))
self.assertIsInstance(res, set)
# my_facts is in minimal_gather_subset, so always returned
self.assertEqual(res, set(['my_fact', 'something_else']))
def test_gather_subset_excludes_ordering(self):
valid_subsets = frozenset(['my_fact', 'something_else', 'whatever'])
minimal_gather_subset = frozenset(['my_fact'])
res = collector.get_collector_names(valid_subsets=valid_subsets,
minimal_gather_subset=minimal_gather_subset,
gather_subset=set(['!all', 'whatever']))
self.assertIsInstance(res, set)
# excludes are higher precedence than includes, so !all excludes everything
# and then minimal_gather_subset is added. so '!all', 'other' == '!all'
self.assertEqual(res, set(['my_fact']))
def test_invaid_gather_subset(self):
valid_subsets = frozenset(['my_fact', 'something_else'])
minimal_gather_subset = frozenset(['my_fact'])
self.assertRaisesRegexp(TypeError,
'Bad subset .* given to Ansible.*allowed\:.*all,.*my_fact.*',
collector.get_collector_names,
valid_subsets=valid_subsets,
minimal_gather_subset=minimal_gather_subset,
gather_subset=set(['my_fact', 'not_a_valid_gather_subset']))
class TestCollectorClassesFromGatherSubset(unittest.TestCase):
def _classes(self,
all_collector_classes=None,
valid_subsets=None,
minimal_gather_subset=None,
gather_subset=None,
gather_timeout=None):
return collector.collector_classes_from_gather_subset(all_collector_classes=all_collector_classes,
valid_subsets=valid_subsets,
minimal_gather_subset=minimal_gather_subset,
gather_subset=gather_subset,
gather_timeout=gather_timeout)
def test_no_args(self):
res = self._classes()
self.assertIsInstance(res, list)
self.assertEqual(res, [])
def test(self):
res = self._classes(all_collector_classes=default_collectors.collectors,
gather_subset=set(['!all']))
self.assertIsInstance(res, list)
self.assertEqual(res, [])
def test_env(self):
res = self._classes(all_collector_classes=default_collectors.collectors,
gather_subset=set(['env']))
self.assertIsInstance(res, list)
self.assertEqual(res, [default_collectors.EnvFactCollector])
def test_collector_specified_multiple_times(self):
res = self._classes(all_collector_classes=default_collectors.collectors,
gather_subset=set(['platform', 'all', 'machine']))
self.assertIsInstance(res, list)
self.assertIn(default_collectors.PlatformFactCollector,
res)
def test_unknown_collector(self):
# something claims 'unknown_collector' is a valid gather_subset, but there is
# no FactCollector mapped to 'unknown_collector'
self.assertRaisesRegexp(TypeError,
'Bad subset.*unknown_collector.*given to Ansible.*allowed\:.*all,.*env.*',
self._classes,
all_collector_classes=default_collectors.collectors,
gather_subset=set(['env', 'unknown_collector']))

@ -0,0 +1,330 @@
# unit tests for ansible fact collectors
# -*- coding: utf-8 -*-
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
# Make coding more python3-ish
from __future__ import (absolute_import, division)
__metaclass__ = type
from ansible.compat.tests.mock import Mock, patch
from . base import BaseFactsTest
from ansible.module_utils.facts import collector
from ansible.module_utils.facts.system.apparmor import ApparmorFactCollector
from ansible.module_utils.facts.system.caps import SystemCapabilitiesFactCollector
from ansible.module_utils.facts.system.cmdline import CmdLineFactCollector
from ansible.module_utils.facts.system.distribution import DistributionFactCollector
from ansible.module_utils.facts.system.dns import DnsFactCollector
from ansible.module_utils.facts.system.env import EnvFactCollector
from ansible.module_utils.facts.system.fips import FipsFactCollector
from ansible.module_utils.facts.system.pkg_mgr import PkgMgrFactCollector
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.virtual.base import VirtualCollector
from ansible.module_utils.facts.network.base import NetworkCollector
from ansible.module_utils.facts.hardware.base import HardwareCollector
class CollectorException(Exception):
pass
class ExceptionThrowingCollector(collector.BaseFactCollector):
name = 'exc_throwing'
def __init__(self, collectors=None, namespace=None, exception=None):
super(ExceptionThrowingCollector, self).__init__(collectors, namespace)
self._exception = exception or CollectorException('collection failed')
def collect(self, module=None, collected_facts=None):
raise self._exception
class TestExceptionThrowingCollector(BaseFactsTest):
__test__ = True
gather_subset = ['exc_throwing']
valid_subsets = ['exc_throwing']
collector_class = ExceptionThrowingCollector
def test_collect(self):
module = self._mock_module()
fact_collector = self.collector_class()
self.assertRaises(CollectorException,
fact_collector.collect,
module=module,
collected_facts=self.collected_facts)
def test_collect_with_namespace(self):
module = self._mock_module()
fact_collector = self.collector_class()
self.assertRaises(CollectorException,
fact_collector.collect_with_namespace,
module=module,
collected_facts=self.collected_facts)
class TestApparmorFacts(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'apparmor']
valid_subsets = ['apparmor']
fact_namespace = 'ansible_apparmor'
collector_class = ApparmorFactCollector
def test_collect(self):
facts_dict = super(TestApparmorFacts, self).test_collect()
self.assertIn('status', facts_dict['apparmor'])
class TestCapsFacts(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'caps']
valid_subsets = ['caps']
fact_namespace = 'ansible_system_capabilities'
collector_class = SystemCapabilitiesFactCollector
def _mock_module(self):
mock_module = Mock()
mock_module.params = {'gather_subset': self.gather_subset,
'gather_timeout': 10,
'filter': '*'}
mock_module.get_bin_path = Mock(return_value='/usr/sbin/capsh')
mock_module.run_command = Mock(return_value=(0, 'Current: =ep', ''))
return mock_module
class TestCmdLineFacts(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'cmdline']
valid_subsets = ['cmdline']
fact_namespace = 'ansible_cmdline'
collector_class = CmdLineFactCollector
class TestDistributionFacts(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'distribution']
valid_subsets = ['distribution']
fact_namespace = 'ansible_distribution'
collector_class = DistributionFactCollector
class TestDnsFacts(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'dns']
valid_subsets = ['dns']
fact_namespace = 'ansible_dns'
collector_class = DnsFactCollector
class TestEnvFacts(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'env']
valid_subsets = ['env']
fact_namespace = 'ansible_env'
collector_class = EnvFactCollector
def test_collect(self):
facts_dict = super(TestEnvFacts, self).test_collect()
self.assertIn('HOME', facts_dict['env'])
class TestFipsFacts(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'fips']
valid_subsets = ['fips']
fact_namespace = 'ansible_fips'
collector_class = FipsFactCollector
class TestHardwareCollector(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'hardware']
valid_subsets = ['hardware']
fact_namespace = 'ansible_hardware'
collector_class = HardwareCollector
collected_facts = {'ansible_architecture': 'x86_64'}
class TestNetworkCollector(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'network']
valid_subsets = ['network']
fact_namespace = 'ansible_network'
collector_class = NetworkCollector
class TestPkgMgrFacts(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'pkg_mgr']
valid_subsets = ['pkg_mgr']
fact_namespace = 'ansible_pkgmgr'
collector_class = PkgMgrFactCollector
class TestPlatformFactCollector(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'platform']
valid_subsets = ['platform']
fact_namespace = 'ansible_platform'
collector_class = PlatformFactCollector
class TestPythonFactCollector(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'python']
valid_subsets = ['python']
fact_namespace = 'ansible_python'
collector_class = PythonFactCollector
class TestSelinuxFacts(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'selinux']
valid_subsets = ['selinux']
fact_namespace = 'ansible_selinux'
collector_class = SelinuxFactCollector
def test_no_selinux(self):
with patch('ansible.module_utils.facts.system.selinux.HAVE_SELINUX', False):
module = self._mock_module()
fact_collector = self.collector_class()
facts_dict = fact_collector.collect(module=module)
self.assertIsInstance(facts_dict, dict)
self.assertFalse(facts_dict['selinux'])
return facts_dict
class TestServiceMgrFacts(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'service_mgr']
valid_subsets = ['service_mgr']
fact_namespace = 'ansible_service_mgr'
collector_class = ServiceMgrFactCollector
# TODO: dedupe some of this test code
@patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value=None)
def test_no_proc1(self, mock_gfc):
# no /proc/1/comm, ps returns non-0
# should fallback to 'service'
module = self._mock_module()
module.run_command = Mock(return_value=(1, '', 'wat'))
fact_collector = self.collector_class()
facts_dict = fact_collector.collect(module=module)
self.assertIsInstance(facts_dict, dict)
self.assertEqual(facts_dict['service_mgr'], 'service')
@patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value=None)
def test_no_proc1_ps_random_init(self, mock_gfc):
# no /proc/1/comm, ps returns '/sbin/sys11' which we dont know
# should end up return 'sys11'
module = self._mock_module()
module.run_command = Mock(return_value=(0, '/sbin/sys11', ''))
fact_collector = self.collector_class()
facts_dict = fact_collector.collect(module=module)
self.assertIsInstance(facts_dict, dict)
self.assertEqual(facts_dict['service_mgr'], 'sys11')
@patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value=None)
def test_clowncar(self, mock_gfc):
# no /proc/1/comm, ps fails, distro and system are clowncar
# should end up return 'sys11'
module = self._mock_module()
module.run_command = Mock(return_value=(1, '', ''))
collected_facts = {'distribution': 'clowncar',
'system': 'ClownCarOS'}
fact_collector = self.collector_class()
facts_dict = fact_collector.collect(module=module,
collected_facts=collected_facts)
self.assertIsInstance(facts_dict, dict)
self.assertEqual(facts_dict['service_mgr'], 'service')
# TODO: reenable these tests when we can mock more easily
# @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value=None)
# def test_sunos_fallback(self, mock_gfc):
# # no /proc/1/comm, ps fails, 'system' is SunOS
# # should end up return 'smf'?
# module = self._mock_module()
# # FIXME: the result here is a kluge to at least cover more of service_mgr.collect
# # TODO: remove
# # FIXME: have to force a pid for results here to get into any of the system/distro checks
# module.run_command = Mock(return_value=(1, ' 37 ', ''))
# collected_facts = {'system': 'SunOS'}
# fact_collector = self.collector_class(module=module)
# facts_dict = fact_collector.collect(collected_facts=collected_facts)
# print('facts_dict: %s' % facts_dict)
# self.assertIsInstance(facts_dict, dict)
# self.assertEqual(facts_dict['service_mgr'], 'smf')
# @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value=None)
# def test_aix_fallback(self, mock_gfc):
# # no /proc/1/comm, ps fails, 'system' is SunOS
# # should end up return 'smf'?
# module = self._mock_module()
# module.run_command = Mock(return_value=(1, '', ''))
# collected_facts = {'system': 'AIX'}
# fact_collector = self.collector_class(module=module)
# facts_dict = fact_collector.collect(collected_facts=collected_facts)
# print('facts_dict: %s' % facts_dict)
# self.assertIsInstance(facts_dict, dict)
# self.assertEqual(facts_dict['service_mgr'], 'src')
# @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value=None)
# def test_linux_fallback(self, mock_gfc):
# # no /proc/1/comm, ps fails, 'system' is SunOS
# # should end up return 'smf'?
# module = self._mock_module()
# module.run_command = Mock(return_value=(1, ' 37 ', ''))
# collected_facts = {'system': 'Linux'}
# fact_collector = self.collector_class(module=module)
# facts_dict = fact_collector.collect(collected_facts=collected_facts)
# print('facts_dict: %s' % facts_dict)
# self.assertIsInstance(facts_dict, dict)
# self.assertEqual(facts_dict['service_mgr'], 'sdfadf')
class TestSshPubKeyFactCollector(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'ssh_pub_keys']
valid_subsets = ['ssh_pub_keys']
fact_namespace = 'ansible_ssh_pub_leys'
collector_class = SshPubKeyFactCollector
class TestUserFactCollector(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'user']
valid_subsets = ['user']
fact_namespace = 'ansible_user'
collector_class = UserFactCollector
class TestVirtualFacts(BaseFactsTest):
__test__ = True
gather_subset = ['!all', 'virtual']
valid_subsets = ['virtual']
fact_namespace = 'ansible_virtual'
collector_class = VirtualCollector

@ -1,4 +1,6 @@
# This file is part of Ansible
# -*- coding: utf-8 -*-
#
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -12,6 +14,7 @@
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
# Make coding more python3-ish
from __future__ import (absolute_import, division)
@ -19,26 +22,36 @@ __metaclass__ = type
import os
import pytest
# for testing
from ansible.compat.tests import unittest
from ansible.compat.tests.mock import Mock, patch
from ansible.module_utils import facts
from ansible.module_utils.facts import hardware
from ansible.module_utils.facts import network
from ansible.module_utils.facts import virtual
class BaseTestFactsPlatform(unittest.TestCase):
platform_id = 'Generic'
fact_class = facts.Hardware
fact_class = hardware.base.Hardware
collector_class = None
"""Verify that the automagic in Hardware.__new__ selects the right subclass."""
@patch('platform.system')
def test_new(self, mock_platform):
if not self.fact_class:
pytest.skip('This platform (%s) does not have a fact_class.' % self.platform_id)
mock_platform.return_value = self.platform_id
inst = self.fact_class(module=Mock(), load_on_init=False)
self.assertIsInstance(inst, self.fact_class)
self.assertEqual(inst.platform, self.platform_id)
def test_subclass(self):
if not self.fact_class:
pytest.skip('This platform (%s) does not have a fact_class.' % self.platform_id)
# 'Generic' will try to map to platform.system() that we are not mocking here
if self.platform_id == 'Generic':
return
@ -46,130 +59,179 @@ class BaseTestFactsPlatform(unittest.TestCase):
self.assertIsInstance(inst, self.fact_class)
self.assertEqual(inst.platform, self.platform_id)
def test_collector(self):
if not self.collector_class:
pytest.skip('This test class needs to be updated to specify collector_class')
inst = self.collector_class()
self.assertIsInstance(inst, self.collector_class)
self.assertEqual(inst._platform, self.platform_id)
class TestLinuxFactsPlatform(BaseTestFactsPlatform):
platform_id = 'Linux'
fact_class = facts.LinuxHardware
fact_class = hardware.linux.LinuxHardware
collector_class = hardware.linux.LinuxHardwareCollector
class TestHurdFactsPlatform(BaseTestFactsPlatform):
platform_id = 'GNU'
fact_class = hardware.hurd.HurdHardware
collector_class = hardware.hurd.HurdHardwareCollector
class TestSunOSHardware(BaseTestFactsPlatform):
platform_id = 'SunOS'
fact_class = facts.SunOSHardware
fact_class = hardware.sunos.SunOSHardware
collector_class = hardware.sunos.SunOSHardwareCollector
class TestOpenBSDHardware(BaseTestFactsPlatform):
platform_id = 'OpenBSD'
fact_class = facts.OpenBSDHardware
fact_class = hardware.openbsd.OpenBSDHardware
collector_class = hardware.openbsd.OpenBSDHardwareCollector
class TestFreeBSDHardware(BaseTestFactsPlatform):
platform_id = 'FreeBSD'
fact_class = facts.FreeBSDHardware
fact_class = hardware.freebsd.FreeBSDHardware
collector_class = hardware.freebsd.FreeBSDHardwareCollector
class TestDragonFlyHardware(BaseTestFactsPlatform):
platform_id = 'DragonFly'
fact_class = facts.DragonFlyHardware
fact_class = None
collector_class = hardware.dragonfly.DragonFlyHardwareCollector
class TestNetBSDHardware(BaseTestFactsPlatform):
platform_id = 'NetBSD'
fact_class = facts.NetBSDHardware
fact_class = hardware.netbsd.NetBSDHardware
collector_class = hardware.netbsd.NetBSDHardwareCollector
class TestAIXHardware(BaseTestFactsPlatform):
platform_id = 'AIX'
fact_class = facts.AIX
fact_class = hardware.aix.AIXHardware
collector_class = hardware.aix.AIXHardwareCollector
class TestHPUXHardware(BaseTestFactsPlatform):
platform_id = 'HP-UX'
fact_class = facts.HPUX
fact_class = hardware.hpux.HPUXHardware
collector_class = hardware.hpux.HPUXHardwareCollector
class TestDarwinHardware(BaseTestFactsPlatform):
platform_id = 'Darwin'
fact_class = facts.Darwin
fact_class = hardware.darwin.DarwinHardware
collector_class = hardware.darwin.DarwinHardwareCollector
class TestGenericNetwork(BaseTestFactsPlatform):
platform_id = 'Generic'
fact_class = facts.Network
fact_class = network.base.Network
class TestHurdPfinetNetwork(BaseTestFactsPlatform):
platform_id = 'GNU'
fact_class = network.hurd.HurdPfinetNetwork
collector_class = network.hurd.HurdNetworkCollector
class TestLinuxNetwork(BaseTestFactsPlatform):
platform_id = 'Generic'
fact_class = facts.Network
platform_id = 'Linux'
fact_class = network.linux.LinuxNetwork
collector_class = network.linux.LinuxNetworkCollector
class TestGenericBsdIfconfigNetwork(BaseTestFactsPlatform):
platform_id = 'Generic_BSD_Ifconfig'
fact_class = facts.GenericBsdIfconfigNetwork
fact_class = network.generic_bsd.GenericBsdIfconfigNetwork
collector_class = None
class TestHPUXNetwork(BaseTestFactsPlatform):
platform_id = 'HP-UX'
fact_class = facts.HPUXNetwork
fact_class = network.hpux.HPUXNetwork
collector_class = network.hpux.HPUXNetworkCollector
class TestDarwinNetwork(BaseTestFactsPlatform):
platform_id = 'Darwin'
fact_class = facts.DarwinNetwork
fact_class = network.darwin.DarwinNetwork
collector_class = network.darwin.DarwinNetworkCollector
class TestFreeBSDNetwork(BaseTestFactsPlatform):
platform_id = 'FreeBSD'
fact_class = facts.FreeBSDNetwork
fact_class = network.freebsd.FreeBSDNetwork
collector_class = network.freebsd.FreeBSDNetworkCollector
class TestDragonFlyNetwork(BaseTestFactsPlatform):
platform_id = 'DragonFly'
fact_class = facts.DragonFlyNetwork
fact_class = network.dragonfly.DragonFlyNetwork
collector_class = network.dragonfly.DragonFlyNetworkCollector
class TestAIXNetwork(BaseTestFactsPlatform):
platform_id = 'AIX'
fact_class = facts.AIXNetwork
fact_class = network.aix.AIXNetwork
collector_class = network.aix.AIXNetworkCollector
class TestNetBSDNetwork(BaseTestFactsPlatform):
platform_id = 'NetBSD'
fact_class = network.netbsd.NetBSDNetwork
collector_class = network.netbsd.NetBSDNetworkCollector
class TestOpenBSDNetwork(BaseTestFactsPlatform):
platform_id = 'OpenBSD'
fact_class = facts.OpenBSDNetwork
fact_class = network.openbsd.OpenBSDNetwork
collector_class = network.openbsd.OpenBSDNetworkCollector
class TestSunOSNetwork(BaseTestFactsPlatform):
platform_id = 'SunOS'
fact_class = facts.SunOSNetwork
fact_class = network.sunos.SunOSNetwork
collector_class = network.sunos.SunOSNetworkCollector
class TestLinuxVirtual(BaseTestFactsPlatform):
platform_id = 'Linux'
fact_class = facts.LinuxVirtual
fact_class = virtual.linux.LinuxVirtual
collector_class = virtual.linux.LinuxVirtualCollector
class TestFreeBSDVirtual(BaseTestFactsPlatform):
platform_id = 'FreeBSD'
fact_class = facts.FreeBSDNetwork
fact_class = virtual.freebsd.FreeBSDVirtual
collector_class = virtual.freebsd.FreeBSDVirtualCollector
class TestDragonFlyVirtual(BaseTestFactsPlatform):
platform_id = 'DragonFly'
fact_class = facts.DragonFlyNetwork
class TestNetBSDVirtual(BaseTestFactsPlatform):
platform_id = 'NetBSD'
fact_class = virtual.netbsd.NetBSDVirtual
collector_class = virtual.netbsd.NetBSDVirtualCollector
class TestOpenBSDVirtual(BaseTestFactsPlatform):
platform_id = 'OpenBSD'
fact_class = facts.OpenBSDVirtual
fact_class = virtual.openbsd.OpenBSDVirtual
collector_class = virtual.openbsd.OpenBSDVirtualCollector
class TestHPUXVirtual(BaseTestFactsPlatform):
platform_id = 'HP-UX'
fact_class = facts.HPUXVirtual
fact_class = virtual.hpux.HPUXVirtual
collector_class = virtual.hpux.HPUXVirtualCollector
class TestSunOSVirtual(BaseTestFactsPlatform):
platform_id = 'SunOS'
fact_class = facts.SunOSVirtual
fact_class = virtual.sunos.SunOSVirtual
collector_class = virtual.sunos.SunOSVirtualCollector
LSBLK_OUTPUT = b"""
@ -471,38 +533,38 @@ class TestFactsLinuxHardwareGetMountFacts(unittest.TestCase):
# The Hardware subclasses freakout if instaniated directly, so
# mock platform.system and inst Hardware() so we get a LinuxHardware()
# we can test.
@patch('ansible.module_utils.facts.LinuxHardware._mtab_entries', return_value=MTAB_ENTRIES)
@patch('ansible.module_utils.facts.LinuxHardware._find_bind_mounts', return_value=BIND_MOUNTS)
@patch('ansible.module_utils.facts.LinuxHardware._lsblk_uuid', return_value=LSBLK_UUIDS)
@patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._mtab_entries', return_value=MTAB_ENTRIES)
@patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._find_bind_mounts', return_value=BIND_MOUNTS)
@patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._lsblk_uuid', return_value=LSBLK_UUIDS)
def test_get_mount_facts(self,
mock_lsblk_uuid,
mock_find_bind_mounts,
mock_mtab_entries):
module = Mock()
# Returns a LinuxHardware-ish
lh = facts.LinuxHardware(module=module, load_on_init=False)
lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
# Nothing returned, just self.facts modified as a side effect
lh.get_mount_facts()
self.assertIsInstance(lh.facts, dict)
self.assertIn('mounts', lh.facts)
self.assertIsInstance(lh.facts['mounts'], list)
self.assertIsInstance(lh.facts['mounts'][0], dict)
mount_facts = lh.get_mount_facts()
self.assertIsInstance(mount_facts, dict)
self.assertIn('mounts', mount_facts)
self.assertIsInstance(mount_facts['mounts'], list)
self.assertIsInstance(mount_facts['mounts'][0], dict)
@patch('ansible.module_utils.facts.get_file_content', return_value=MTAB)
@patch('ansible.module_utils.facts.hardware.linux.get_file_content', return_value=MTAB)
def test_get_mtab_entries(self, mock_get_file_content):
module = Mock()
lh = facts.LinuxHardware(module=module, load_on_init=False)
lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
mtab_entries = lh._mtab_entries()
self.assertIsInstance(mtab_entries, list)
self.assertIsInstance(mtab_entries[0], list)
self.assertEqual(len(mtab_entries), 38)
@patch('ansible.module_utils.facts.LinuxHardware._run_findmnt', return_value=(0, FINDMNT_OUTPUT, ''))
@patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._run_findmnt', return_value=(0, FINDMNT_OUTPUT, ''))
def test_find_bind_mounts(self, mock_run_findmnt):
module = Mock()
lh = facts.LinuxHardware(module=module, load_on_init=False)
lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
bind_mounts = lh._find_bind_mounts()
# If bind_mounts becomes another seq type, feel free to change
@ -510,10 +572,10 @@ class TestFactsLinuxHardwareGetMountFacts(unittest.TestCase):
self.assertEqual(len(bind_mounts), 1)
self.assertIn('/not/a/real/bind_mount', bind_mounts)
@patch('ansible.module_utils.facts.LinuxHardware._run_findmnt', return_value=(37, '', ''))
@patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._run_findmnt', return_value=(37, '', ''))
def test_find_bind_mounts_non_zero(self, mock_run_findmnt):
module = Mock()
lh = facts.LinuxHardware(module=module, load_on_init=False)
lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
bind_mounts = lh._find_bind_mounts()
self.assertIsInstance(bind_mounts, set)
@ -522,48 +584,48 @@ class TestFactsLinuxHardwareGetMountFacts(unittest.TestCase):
def test_find_bind_mounts_no_findmnts(self):
module = Mock()
module.get_bin_path = Mock(return_value=None)
lh = facts.LinuxHardware(module=module, load_on_init=False)
lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
bind_mounts = lh._find_bind_mounts()
self.assertIsInstance(bind_mounts, set)
self.assertEqual(len(bind_mounts), 0)
@patch('ansible.module_utils.facts.LinuxHardware._run_lsblk', return_value=(0, LSBLK_OUTPUT, ''))
@patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._run_lsblk', return_value=(0, LSBLK_OUTPUT, ''))
def test_lsblk_uuid(self, mock_run_lsblk):
module = Mock()
lh = facts.LinuxHardware(module=module, load_on_init=False)
lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
lsblk_uuids = lh._lsblk_uuid()
self.assertIsInstance(lsblk_uuids, dict)
self.assertIn(b'/dev/loop9', lsblk_uuids)
self.assertIn(b'/dev/sda1', lsblk_uuids)
self.assertEquals(lsblk_uuids[b'/dev/sda1'], b'32caaec3-ef40-4691-a3b6-438c3f9bc1c0')
self.assertEqual(lsblk_uuids[b'/dev/sda1'], b'32caaec3-ef40-4691-a3b6-438c3f9bc1c0')
@patch('ansible.module_utils.facts.LinuxHardware._run_lsblk', return_value=(37, LSBLK_OUTPUT, ''))
@patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._run_lsblk', return_value=(37, LSBLK_OUTPUT, ''))
def test_lsblk_uuid_non_zero(self, mock_run_lsblk):
module = Mock()
lh = facts.LinuxHardware(module=module, load_on_init=False)
lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
lsblk_uuids = lh._lsblk_uuid()
self.assertIsInstance(lsblk_uuids, dict)
self.assertEquals(len(lsblk_uuids), 0)
self.assertEqual(len(lsblk_uuids), 0)
def test_lsblk_uuid_no_lsblk(self):
module = Mock()
module.get_bin_path = Mock(return_value=None)
lh = facts.LinuxHardware(module=module, load_on_init=False)
lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
lsblk_uuids = lh._lsblk_uuid()
self.assertIsInstance(lsblk_uuids, dict)
self.assertEquals(len(lsblk_uuids), 0)
self.assertEqual(len(lsblk_uuids), 0)
@patch('ansible.module_utils.facts.LinuxHardware._run_lsblk', return_value=(0, LSBLK_OUTPUT_2, ''))
@patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._run_lsblk', return_value=(0, LSBLK_OUTPUT_2, ''))
def test_lsblk_uuid_dev_with_space_in_name(self, mock_run_lsblk):
module = Mock()
lh = facts.LinuxHardware(module=module, load_on_init=False)
lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
lsblk_uuids = lh._lsblk_uuid()
self.assertIsInstance(lsblk_uuids, dict)
self.assertIn(b'/dev/loop0', lsblk_uuids)
self.assertIn(b'/dev/sda1', lsblk_uuids)
self.assertEquals(lsblk_uuids[b'/dev/mapper/an-example-mapper with a space in the name'], b'84639acb-013f-4d2f-9392-526a572b4373')
self.assertEquals(lsblk_uuids[b'/dev/sda1'], b'32caaec3-ef40-4691-a3b6-438c3f9bc1c0')
self.assertEqual(lsblk_uuids[b'/dev/mapper/an-example-mapper with a space in the name'], b'84639acb-013f-4d2f-9392-526a572b4373')
self.assertEqual(lsblk_uuids[b'/dev/sda1'], b'32caaec3-ef40-4691-a3b6-438c3f9bc1c0')

@ -27,40 +27,40 @@ import pytest
from ansible.compat.tests import unittest
from ansible.compat.tests.mock import patch, MagicMock
from ansible.module_utils import facts
from ansible.module_utils.facts import timeout
@pytest.fixture
def set_gather_timeout_higher():
default_timeout = facts.GATHER_TIMEOUT
facts.GATHER_TIMEOUT = facts.DEFAULT_GATHER_TIMEOUT + 5
default_timeout = timeout.GATHER_TIMEOUT
timeout.GATHER_TIMEOUT = timeout.DEFAULT_GATHER_TIMEOUT + 5
yield
facts.GATHER_TIMEOUT = default_timeout
timeout.GATHER_TIMEOUT = default_timeout
@pytest.fixture
def set_gather_timeout_lower():
default_timeout = facts.GATHER_TIMEOUT
facts.GATHER_TIMEOUT = 2
default_timeout = timeout.GATHER_TIMEOUT
timeout.GATHER_TIMEOUT = 2
yield
facts.GATHER_TIMEOUT = default_timeout
timeout.GATHER_TIMEOUT = default_timeout
@facts.timeout
@timeout.timeout
def sleep_amount_implicit(amount):
# implicit refers to the lack of argument to the decorator
time.sleep(amount)
return 'Succeeded after {0} sec'.format(amount)
@facts.timeout(facts.DEFAULT_GATHER_TIMEOUT + 5)
@timeout.timeout(timeout.DEFAULT_GATHER_TIMEOUT + 5)
def sleep_amount_explicit_higher(amount):
# explicit refers to the argument to the decorator
time.sleep(amount)
return 'Succeeded after {0} sec'.format(amount)
@facts.timeout(2)
@timeout.timeout(2)
def sleep_amount_explicit_lower(amount):
# explicit refers to the argument to the decorator
time.sleep(amount)
@ -71,7 +71,7 @@ def test_defaults_still_within_bounds():
# If the default changes outside of these bounds, some of the tests will
# no longer test the right thing. Need to review and update the timeouts
# in the other tests if this fails
assert facts.DEFAULT_GATHER_TIMEOUT >= 4
assert timeout.DEFAULT_GATHER_TIMEOUT >= 4
def test_implicit_file_default_succeeds():
@ -81,32 +81,32 @@ def test_implicit_file_default_succeeds():
def test_implicit_file_default_timesout():
# sleep_time is greater than the default
sleep_time = facts.DEFAULT_GATHER_TIMEOUT + 1
with pytest.raises(facts.TimeoutError):
sleep_time = timeout.DEFAULT_GATHER_TIMEOUT + 1
with pytest.raises(timeout.TimeoutError):
assert sleep_amount_implicit(sleep_time) == '(Not expected to succeed)'
def test_implicit_file_overridden_succeeds(set_gather_timeout_higher):
# Set sleep_time greater than the default timeout and less than our new timeout
sleep_time = facts.DEFAULT_GATHER_TIMEOUT + 1
sleep_time = timeout.DEFAULT_GATHER_TIMEOUT + 1
assert sleep_amount_implicit(sleep_time) == 'Succeeded after {0} sec'.format(sleep_time)
def test_implicit_file_overridden_timesout(set_gather_timeout_lower):
# Set sleep_time greater than our new timeout but less than the default
sleep_time = 3
with pytest.raises(facts.TimeoutError):
with pytest.raises(timeout.TimeoutError):
assert sleep_amount_implicit(sleep_time) == '(Not expected to Succeed)'
def test_explicit_succeeds():
# Set sleep_time greater than the default timeout and less than our new timeout
sleep_time = facts.DEFAULT_GATHER_TIMEOUT + 1
sleep_time = timeout.DEFAULT_GATHER_TIMEOUT + 1
assert sleep_amount_explicit_higher(sleep_time) == 'Succeeded after {0} sec'.format(sleep_time)
def test_explicit_timeout():
# Set sleep_time greater than our new timeout but less than the default
sleep_time = 3
with pytest.raises(facts.TimeoutError):
with pytest.raises(timeout.TimeoutError):
assert sleep_amount_explicit_lower(sleep_time) == '(Not expected to succeed)'

@ -18,7 +18,6 @@
from __future__ import (absolute_import, division)
__metaclass__ = type
import sys
# to work around basic.py reading stdin
import json
@ -27,12 +26,10 @@ import pytest
from units.mock.procenv import swap_stdin_and_argv
# for testing
from ansible.compat.tests import unittest
from ansible.compat.tests.mock import patch
# the module we are actually testing
import ansible.module_utils.facts as facts
# the module we are actually testing (sort of
from ansible.module_utils.facts.system.distribution import DistributionFactCollector
# to generate the testcase data, you can use the script gen_distribution_version_testcase.py in hacking/tests
TESTSETS = [
@ -485,7 +482,7 @@ VERSION_ID="12.04"
"name": "KDE neon 16.04",
"result": {
"distribution_release": "xenial",
"distribution": "Neon",
"distribution": "KDE neon",
"distribution_major_version": "16",
"os_family": "Debian",
"distribution_version": "16.04"
@ -512,6 +509,7 @@ DISTRIB_DESCRIPTION="CoreOS 976.0.0 (Coeur Rouge)"
""",
},
'platform.dist': ('', '', ''),
'platform.release': '',
'result': {
"distribution": "CoreOS",
"distribution_major_version": "NA",
@ -613,6 +611,7 @@ DISTRIB_DESCRIPTION="CoreOS 976.0.0 (Coeur Rouge)"
"",
""
],
# "platform.release": 'OmniOS',
"input": {
"/etc/release": (
" OmniOS v11 r151012\n Copyright 2014 OmniTI Computer Consulting, Inc. All rights reserved.\n Use is subject to license terms.\n\n"
@ -634,6 +633,7 @@ DISTRIB_DESCRIPTION="CoreOS 976.0.0 (Coeur Rouge)"
"",
""
],
"platform.release:": "",
"input": {
"/etc/release": (" Open Storage Appliance v3.1.6\n Copyright (c) 2014 Nexenta Systems, Inc. "
"All Rights Reserved.\n Copyright (c) 2011 Oracle. All Rights Reserved.\n "
@ -838,10 +838,10 @@ def test_distribution_version(testcase):
basic._ANSIBLE_ARGS = None
module = basic.AnsibleModule(argument_spec=dict())
_test_one_distribution(facts, module, testcase)
_test_one_distribution(module, testcase)
def _test_one_distribution(facts, module, testcase):
def _test_one_distribution(module, testcase):
"""run the test on one distribution testcase
* prepare some mock functions to get the testdata in
@ -863,27 +863,34 @@ def _test_one_distribution(facts, module, testcase):
def mock_get_uname_version(module):
return testcase.get('uname_v', None)
def mock_path_exists(fname):
return fname in testcase['input']
def mock_file_exists(fname, allow_empty=False):
if fname not in testcase['input']:
return False
def mock_path_getsize(fname):
if fname in testcase['input']:
# the len is not used, but why not be honest if you can be?
return len(testcase['input'][fname])
else:
return 0
if allow_empty:
return True
return bool(len(testcase['input'][fname]))
def mock_platform_system():
return testcase.get('platform.system', 'Linux')
@patch('ansible.module_utils.facts.get_file_content', mock_get_file_content)
@patch('ansible.module_utils.facts.get_uname_version', mock_get_uname_version)
@patch('os.path.exists', mock_path_exists)
@patch('os.path.getsize', mock_path_getsize)
def mock_platform_release():
return testcase.get('platform.release', '')
def mock_platform_version():
return testcase.get('platform.version', '')
@patch('ansible.module_utils.facts.system.distribution.get_file_content', mock_get_file_content)
@patch('ansible.module_utils.facts.system.distribution.get_uname_version', mock_get_uname_version)
@patch('ansible.module_utils.facts.system.distribution._file_exists', mock_file_exists)
@patch('platform.dist', lambda: testcase['platform.dist'])
@patch('platform.system', mock_platform_system)
@patch('platform.release', mock_platform_release)
@patch('platform.version', mock_platform_version)
def get_facts(testcase):
return facts.Facts(module).populate()
distro_collector = DistributionFactCollector()
res = distro_collector.collect(module)
return res
generated_facts = get_facts(testcase)

@ -0,0 +1,308 @@
# unit tests for ansible/module_utils/facts/__init__.py
# -*- coding: utf-8 -*-
#
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
# Make coding more python3-ish
from __future__ import (absolute_import, division)
__metaclass__ = type
# for testing
from ansible.compat.tests import unittest
from ansible.compat.tests.mock import Mock
from ansible.module_utils.facts import collector
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.date_time import DateTimeFactCollector
from ansible.module_utils.facts.system.env import EnvFactCollector
from ansible.module_utils.facts.system.distribution import DistributionFactCollector
from ansible.module_utils.facts.system.dns import DnsFactCollector
from ansible.module_utils.facts.system.fips import FipsFactCollector
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.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.user import UserFactCollector
# from ansible.module_utils.facts.hardware.base import HardwareCollector
from ansible.module_utils.facts.network.base import NetworkCollector
from ansible.module_utils.facts.virtual.base import VirtualCollector
# module under test
from ansible.modules.system import setup
ALL_COLLECTOR_CLASSES = \
[PlatformFactCollector,
DistributionFactCollector,
SelinuxFactCollector,
ApparmorFactCollector,
SystemCapabilitiesFactCollector,
FipsFactCollector,
PkgMgrFactCollector,
ServiceMgrFactCollector,
LSBFactCollector,
DateTimeFactCollector,
UserFactCollector,
LocalFactCollector,
EnvFactCollector,
DnsFactCollector,
PythonFactCollector,
# FIXME: re-enable when hardware doesnt Hardware() doesnt munge self.facts
# HardwareCollector
NetworkCollector,
VirtualCollector,
OhaiFactCollector,
FacterFactCollector]
def mock_module(gather_subset=None):
if gather_subset is None:
gather_subset = ['all', '!facter', '!ohai']
mock_module = Mock()
mock_module.params = {'gather_subset': gather_subset,
'gather_timeout': 5,
'filter': '*'}
mock_module.get_bin_path = Mock(return_value=None)
return mock_module
def _collectors(module,
all_collector_classes=None,
minimal_gather_subset=None):
gather_subset = module.params.get('gather_subset')
if all_collector_classes is None:
all_collector_classes = ALL_COLLECTOR_CLASSES
if minimal_gather_subset is None:
minimal_gather_subset = 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)
collectors = []
for collector_class in collector_classes:
collector_obj = collector_class()
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 = \
setup.CollectorMetaDataCollector(gather_subset=gather_subset,
module_setup=True)
collectors.append(collector_meta_data_collector)
return collectors
# FIXME: this is brute force, but hopefully enough to get some refactoring to make facts testable
class TestInPlace(unittest.TestCase):
def _mock_module(self, gather_subset=None):
return mock_module(gather_subset=gather_subset)
def _collectors(self, module,
all_collector_classes=None,
minimal_gather_subset=None):
return _collectors(module=module,
all_collector_classes=all_collector_classes,
minimal_gather_subset=minimal_gather_subset)
def test(self):
gather_subset = ['all']
mock_module = self._mock_module(gather_subset=gather_subset)
all_collector_classes = [EnvFactCollector]
collectors = self._collectors(mock_module,
all_collector_classes=all_collector_classes)
fact_collector = \
setup.AnsibleFactCollector(collectors=collectors)
res = fact_collector.collect(module=mock_module)
self.assertIsInstance(res, dict)
self.assertIn('ansible_facts', res)
self.assertIsInstance(res['ansible_facts'], dict)
self.assertIn('env', res['ansible_facts'])
self.assertIn('gather_subset', res['ansible_facts'])
self.assertEqual(res['ansible_facts']['gather_subset'], ['all'])
def test1(self):
gather_subset = ['all']
mock_module = self._mock_module(gather_subset=gather_subset)
collectors = self._collectors(mock_module)
fact_collector = \
setup.AnsibleFactCollector(collectors=collectors)
res = fact_collector.collect(module=mock_module)
self.assertIsInstance(res, dict)
self.assertIn('ansible_facts', res)
# just assert it's not almost empty
# with run_command and get_file_content mock, many facts are empty, like network
self.assertGreater(len(res['ansible_facts']), 20)
def test_empty_all_collector_classes(self):
mock_module = self._mock_module()
all_collector_classes = []
collectors = self._collectors(mock_module,
all_collector_classes=all_collector_classes)
fact_collector = \
setup.AnsibleFactCollector(collectors=collectors)
res = fact_collector.collect()
self.assertIsInstance(res, dict)
self.assertIn('ansible_facts', res)
# just assert it's not almost empty
self.assertLess(len(res['ansible_facts']), 3)
# def test_facts_class(self):
# mock_module = self._mock_module()
# Facts(mock_module)
# def test_facts_class_load_on_init_false(self):
# mock_module = self._mock_module()
# Facts(mock_module, load_on_init=False)
# # FIXME: assert something
class TestCollectedFacts(unittest.TestCase):
gather_subset = ['all', '!facter', '!ohai']
min_fact_count = 30
max_fact_count = 1000
# TODO: add ansible_cmdline, ansible_*_pubkey* back when TempFactCollector goes away
expected_facts = ['date_time',
'user_id', 'distribution',
'gather_subset', 'module_setup',
'env']
not_expected_facts = ['facter', 'ohai']
def _mock_module(self, gather_subset=None):
return mock_module(gather_subset=self.gather_subset)
def setUp(self):
mock_module = self._mock_module()
collectors = self._collectors(mock_module)
fact_collector = \
setup.AnsibleFactCollector(collectors=collectors)
self.facts = fact_collector.collect(module=mock_module)
def _collectors(self, module,
all_collector_classes=None,
minimal_gather_subset=None):
return _collectors(module=module,
all_collector_classes=all_collector_classes,
minimal_gather_subset=minimal_gather_subset)
def test_basics(self):
self._assert_basics(self.facts)
def test_expected_facts(self):
self._assert_expected_facts(self.facts)
def test_not_expected_facts(self):
self._assert_not_expected_facts(self.facts)
def _assert_basics(self, facts):
self.assertIsInstance(facts, dict)
self.assertIn('ansible_facts', facts)
# just assert it's not almost empty
self.assertGreaterEqual(len(facts['ansible_facts']), self.min_fact_count)
# and that is not huge number of keys
self.assertLess(len(facts['ansible_facts']), self.max_fact_count)
# everything starts with ansible_ namespace
def _assert_ansible_namespace(self, facts):
subfacts = facts['ansible_facts']
# FIXME: kluge for non-namespace fact
subfacts.pop('module_setup', None)
subfacts.pop('gather_subset', None)
for fact_key in subfacts:
self.assertTrue(fact_key.startswith('ansible_'),
'The fact name "%s" does not startwith "ansible_"' % fact_key)
def _assert_expected_facts(self, facts):
subfacts = facts['ansible_facts']
import pprint
pprint.pprint(subfacts)
subfacts_keys = sorted(subfacts.keys())
for expected_fact in self.expected_facts:
self.assertIn(expected_fact, subfacts_keys)
# self.assertIsInstance(subfacts['ansible_env'], dict)
# self.assertIsInstance(subfacts['ansible_env'], dict)
# self._assert_ssh_facts(subfacts)
def _assert_not_expected_facts(self, facts):
subfacts = facts['ansible_facts']
subfacts_keys = sorted(subfacts.keys())
for not_expected_fact in self.not_expected_facts:
self.assertNotIn(not_expected_fact, subfacts_keys)
def _assert_ssh_facts(self, subfacts):
self.assertIn('ssh_host_key_rsa_public', subfacts.keys())
class ExceptionThrowingCollector(collector.BaseFactCollector):
def collect(self, module=None, collected_facts=None):
raise Exception('A collector failed')
class TestExceptionCollectedFacts(TestCollectedFacts):
def _collectors(self, module,
all_collector_classes=None,
minimal_gather_subset=None):
collectors = _collectors(module=module,
all_collector_classes=all_collector_classes,
minimal_gather_subset=minimal_gather_subset)
c = [ExceptionThrowingCollector()] + collectors
return c
class TestOnlyExceptionCollector(TestCollectedFacts):
expected_facts = []
min_fact_count = 0
def _collectors(self, module,
all_collector_classes=None,
minimal_gather_subset=None):
return [ExceptionThrowingCollector()]
class TestMinimalCollectedFacts(TestCollectedFacts):
gather_subset = ['!all']
min_fact_count = 1
max_fact_count = 10
expected_facts = ['gather_subset',
'module_setup']
not_expected_facts = ['lsb']
Loading…
Cancel
Save