summaryrefslogtreecommitdiffstats
path: root/lib/ansible/module_utils/facts/network
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/module_utils/facts/network')
-rw-r--r--lib/ansible/module_utils/facts/network/__init__.py0
-rw-r--r--lib/ansible/module_utils/facts/network/aix.py145
-rw-r--r--lib/ansible/module_utils/facts/network/base.py72
-rw-r--r--lib/ansible/module_utils/facts/network/darwin.py49
-rw-r--r--lib/ansible/module_utils/facts/network/dragonfly.py33
-rw-r--r--lib/ansible/module_utils/facts/network/fc_wwn.py111
-rw-r--r--lib/ansible/module_utils/facts/network/freebsd.py33
-rw-r--r--lib/ansible/module_utils/facts/network/generic_bsd.py321
-rw-r--r--lib/ansible/module_utils/facts/network/hpux.py82
-rw-r--r--lib/ansible/module_utils/facts/network/hurd.py87
-rw-r--r--lib/ansible/module_utils/facts/network/iscsi.py115
-rw-r--r--lib/ansible/module_utils/facts/network/linux.py327
-rw-r--r--lib/ansible/module_utils/facts/network/netbsd.py48
-rw-r--r--lib/ansible/module_utils/facts/network/nvme.py57
-rw-r--r--lib/ansible/module_utils/facts/network/openbsd.py42
-rw-r--r--lib/ansible/module_utils/facts/network/sunos.py116
16 files changed, 1638 insertions, 0 deletions
diff --git a/lib/ansible/module_utils/facts/network/__init__.py b/lib/ansible/module_utils/facts/network/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/__init__.py
diff --git a/lib/ansible/module_utils/facts/network/aix.py b/lib/ansible/module_utils/facts/network/aix.py
new file mode 100644
index 0000000..e9c90c6
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/aix.py
@@ -0,0 +1,145 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import re
+
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
+
+
+class AIXNetwork(GenericBsdIfconfigNetwork):
+ """
+ This is the AIX Network Class.
+ It uses the GenericBsdIfconfigNetwork unchanged.
+ """
+ platform = 'AIX'
+
+ def get_default_interfaces(self, route_path):
+ interface = dict(v4={}, v6={})
+
+ netstat_path = self.module.get_bin_path('netstat')
+
+ if netstat_path:
+ rc, out, err = self.module.run_command([netstat_path, '-nr'])
+
+ lines = out.splitlines()
+ for line in lines:
+ words = line.split()
+ if len(words) > 1 and words[0] == 'default':
+ if '.' in words[1]:
+ interface['v4']['gateway'] = words[1]
+ interface['v4']['interface'] = words[5]
+ elif ':' in words[1]:
+ interface['v6']['gateway'] = words[1]
+ interface['v6']['interface'] = words[5]
+
+ return interface['v4'], interface['v6']
+
+ # AIX 'ifconfig -a' does not have three words in the interface line
+ def get_interfaces_info(self, ifconfig_path, ifconfig_options='-a'):
+ interfaces = {}
+ current_if = {}
+ ips = dict(
+ all_ipv4_addresses=[],
+ all_ipv6_addresses=[],
+ )
+
+ uname_rc = None
+ uname_out = None
+ uname_err = None
+ uname_path = self.module.get_bin_path('uname')
+ if uname_path:
+ uname_rc, uname_out, uname_err = self.module.run_command([uname_path, '-W'])
+
+ rc, out, err = self.module.run_command([ifconfig_path, ifconfig_options])
+
+ for line in out.splitlines():
+
+ if line:
+ words = line.split()
+
+ # only this condition differs from GenericBsdIfconfigNetwork
+ if re.match(r'^\w*\d*:', line):
+ current_if = self.parse_interface_line(words)
+ interfaces[current_if['device']] = current_if
+ elif words[0].startswith('options='):
+ self.parse_options_line(words, current_if, ips)
+ elif words[0] == 'nd6':
+ self.parse_nd6_line(words, current_if, ips)
+ elif words[0] == 'ether':
+ self.parse_ether_line(words, current_if, ips)
+ elif words[0] == 'media:':
+ self.parse_media_line(words, current_if, ips)
+ elif words[0] == 'status:':
+ self.parse_status_line(words, current_if, ips)
+ elif words[0] == 'lladdr':
+ self.parse_lladdr_line(words, current_if, ips)
+ elif words[0] == 'inet':
+ self.parse_inet_line(words, current_if, ips)
+ elif words[0] == 'inet6':
+ self.parse_inet6_line(words, current_if, ips)
+ else:
+ self.parse_unknown_line(words, current_if, ips)
+
+ # don't bother with wpars it does not work
+ # zero means not in wpar
+ if not uname_rc and uname_out.split()[0] == '0':
+
+ if current_if['macaddress'] == 'unknown' and re.match('^en', current_if['device']):
+ entstat_path = self.module.get_bin_path('entstat')
+ if entstat_path:
+ rc, out, err = self.module.run_command([entstat_path, current_if['device']])
+ if rc != 0:
+ break
+ for line in out.splitlines():
+ if not line:
+ pass
+ buff = re.match('^Hardware Address: (.*)', line)
+ if buff:
+ current_if['macaddress'] = buff.group(1)
+
+ buff = re.match('^Device Type:', line)
+ if buff and re.match('.*Ethernet', line):
+ current_if['type'] = 'ether'
+
+ # device must have mtu attribute in ODM
+ if 'mtu' not in current_if:
+ lsattr_path = self.module.get_bin_path('lsattr')
+ if lsattr_path:
+ rc, out, err = self.module.run_command([lsattr_path, '-El', current_if['device']])
+ if rc != 0:
+ break
+ for line in out.splitlines():
+ if line:
+ words = line.split()
+ if words[0] == 'mtu':
+ current_if['mtu'] = words[1]
+ return interfaces, ips
+
+ # AIX 'ifconfig -a' does not inform about MTU, so remove current_if['mtu'] here
+ def parse_interface_line(self, words):
+ device = words[0][0:-1]
+ current_if = {'device': device, 'ipv4': [], 'ipv6': [], 'type': 'unknown'}
+ current_if['flags'] = self.get_options(words[1])
+ current_if['macaddress'] = 'unknown' # will be overwritten later
+ return current_if
+
+
+class AIXNetworkCollector(NetworkCollector):
+ _fact_class = AIXNetwork
+ _platform = 'AIX'
diff --git a/lib/ansible/module_utils/facts/network/base.py b/lib/ansible/module_utils/facts/network/base.py
new file mode 100644
index 0000000..8243f06
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/base.py
@@ -0,0 +1,72 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import ansible.module_utils.compat.typing as t
+
+from ansible.module_utils.facts.collector import BaseFactCollector
+
+
+class Network:
+ """
+ This is a generic Network subclass of Facts. This should be further
+ subclassed to implement per platform. If you subclass this,
+ you must define:
+ - interfaces (a list of interface names)
+ - interface_<name> dictionary of ipv4, ipv6, and mac address information.
+
+ All subclasses MUST define platform.
+ """
+ platform = 'Generic'
+
+ # FIXME: remove load_on_init when we can
+ def __init__(self, module, load_on_init=False):
+ self.module = module
+
+ # TODO: more or less abstract/NotImplemented
+ def populate(self, collected_facts=None):
+ return {}
+
+
+class NetworkCollector(BaseFactCollector):
+ # MAYBE: we could try to build this based on the arch specific implementation of Network() or its kin
+ name = 'network'
+ _fact_class = Network
+ _fact_ids = set(['interfaces',
+ 'default_ipv4',
+ 'default_ipv6',
+ 'all_ipv4_addresses',
+ 'all_ipv6_addresses']) # type: t.Set[str]
+
+ IPV6_SCOPE = {'0': 'global',
+ '10': 'host',
+ '20': 'link',
+ '40': 'admin',
+ '50': 'site',
+ '80': 'organization'}
+
+ def collect(self, module=None, collected_facts=None):
+ collected_facts = collected_facts or {}
+ if not module:
+ return {}
+
+ # Network munges cached_facts by side effect, so give it a copy
+ facts_obj = self._fact_class(module)
+
+ facts_dict = facts_obj.populate(collected_facts=collected_facts)
+
+ return facts_dict
diff --git a/lib/ansible/module_utils/facts/network/darwin.py b/lib/ansible/module_utils/facts/network/darwin.py
new file mode 100644
index 0000000..90117e5
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/darwin.py
@@ -0,0 +1,49 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
+
+
+class DarwinNetwork(GenericBsdIfconfigNetwork):
+ """
+ This is the Mac macOS Darwin Network Class.
+ It uses the GenericBsdIfconfigNetwork unchanged
+ """
+ platform = 'Darwin'
+
+ # media line is different to the default FreeBSD one
+ def parse_media_line(self, words, current_if, ips):
+ # not sure if this is useful - we also drop information
+ current_if['media'] = 'Unknown' # Mac does not give us this
+ current_if['media_select'] = words[1]
+ if len(words) > 2:
+ # MacOSX sets the media to '<unknown type>' for bridge interface
+ # and parsing splits this into two words; this if/else helps
+ if words[1] == '<unknown' and words[2] == 'type>':
+ current_if['media_select'] = 'Unknown'
+ current_if['media_type'] = 'unknown type'
+ else:
+ current_if['media_type'] = words[2][1:-1]
+ if len(words) > 3:
+ current_if['media_options'] = self.get_options(words[3])
+
+
+class DarwinNetworkCollector(NetworkCollector):
+ _fact_class = DarwinNetwork
+ _platform = 'Darwin'
diff --git a/lib/ansible/module_utils/facts/network/dragonfly.py b/lib/ansible/module_utils/facts/network/dragonfly.py
new file mode 100644
index 0000000..e43bbb2
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/dragonfly.py
@@ -0,0 +1,33 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
+
+
+class DragonFlyNetwork(GenericBsdIfconfigNetwork):
+ """
+ This is the DragonFly Network Class.
+ It uses the GenericBsdIfconfigNetwork unchanged.
+ """
+ platform = 'DragonFly'
+
+
+class DragonFlyNetworkCollector(NetworkCollector):
+ _fact_class = DragonFlyNetwork
+ _platform = 'DragonFly'
diff --git a/lib/ansible/module_utils/facts/network/fc_wwn.py b/lib/ansible/module_utils/facts/network/fc_wwn.py
new file mode 100644
index 0000000..86182f8
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/fc_wwn.py
@@ -0,0 +1,111 @@
+# Fibre Channel WWN initiator related facts collection for ansible.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+import glob
+
+import ansible.module_utils.compat.typing as t
+
+from ansible.module_utils.facts.utils import get_file_lines
+from ansible.module_utils.facts.collector import BaseFactCollector
+
+
+class FcWwnInitiatorFactCollector(BaseFactCollector):
+ name = 'fibre_channel_wwn'
+ _fact_ids = set() # type: t.Set[str]
+
+ def collect(self, module=None, collected_facts=None):
+ """
+ Example contents /sys/class/fc_host/*/port_name:
+
+ 0x21000014ff52a9bb
+
+ """
+
+ fc_facts = {}
+ fc_facts['fibre_channel_wwn'] = []
+ if sys.platform.startswith('linux'):
+ for fcfile in glob.glob('/sys/class/fc_host/*/port_name'):
+ for line in get_file_lines(fcfile):
+ fc_facts['fibre_channel_wwn'].append(line.rstrip()[2:])
+ elif sys.platform.startswith('sunos'):
+ """
+ on solaris 10 or solaris 11 should use `fcinfo hba-port`
+ TBD (not implemented): on solaris 9 use `prtconf -pv`
+ """
+ cmd = module.get_bin_path('fcinfo')
+ if cmd:
+ cmd = cmd + " hba-port"
+ rc, fcinfo_out, err = module.run_command(cmd)
+ """
+ # fcinfo hba-port | grep "Port WWN"
+ HBA Port WWN: 10000090fa1658de
+ """
+ if rc == 0 and fcinfo_out:
+ for line in fcinfo_out.splitlines():
+ if 'Port WWN' in line:
+ data = line.split(' ')
+ fc_facts['fibre_channel_wwn'].append(data[-1].rstrip())
+ elif sys.platform.startswith('aix'):
+ cmd = module.get_bin_path('lsdev')
+ lscfg_cmd = module.get_bin_path('lscfg')
+ if cmd and lscfg_cmd:
+ # get list of available fibre-channel devices (fcs)
+ cmd = cmd + " -Cc adapter -l fcs*"
+ rc, lsdev_out, err = module.run_command(cmd)
+ if rc == 0 and lsdev_out:
+ for line in lsdev_out.splitlines():
+ # if device is available (not in defined state), get its WWN
+ if 'Available' in line:
+ data = line.split(' ')
+ cmd = lscfg_cmd + " -vl %s" % data[0]
+ rc, lscfg_out, err = module.run_command(cmd)
+ # example output
+ # lscfg -vpl fcs3 | grep "Network Address"
+ # Network Address.............10000090FA551509
+ if rc == 0 and lscfg_out:
+ for line in lscfg_out.splitlines():
+ if 'Network Address' in line:
+ data = line.split('.')
+ fc_facts['fibre_channel_wwn'].append(data[-1].rstrip())
+ elif sys.platform.startswith('hp-ux'):
+ cmd = module.get_bin_path('ioscan')
+ fcmsu_cmd = module.get_bin_path('fcmsutil', opt_dirs=['/opt/fcms/bin'])
+ # go ahead if we have both commands available
+ if cmd and fcmsu_cmd:
+ # ioscan / get list of available fibre-channel devices (fcd)
+ cmd = cmd + " -fnC FC"
+ rc, ioscan_out, err = module.run_command(cmd)
+ if rc == 0 and ioscan_out:
+ for line in ioscan_out.splitlines():
+ line = line.strip()
+ if '/dev/fcd' in line:
+ dev = line.split(' ')
+ # get device information
+ cmd = fcmsu_cmd + " %s" % dev[0]
+ rc, fcmsutil_out, err = module.run_command(cmd)
+ # lookup the following line
+ # N_Port Port World Wide Name = 0x50060b00006975ec
+ if rc == 0 and fcmsutil_out:
+ for line in fcmsutil_out.splitlines():
+ if 'N_Port Port World Wide Name' in line:
+ data = line.split('=')
+ fc_facts['fibre_channel_wwn'].append(data[-1].strip())
+ return fc_facts
diff --git a/lib/ansible/module_utils/facts/network/freebsd.py b/lib/ansible/module_utils/facts/network/freebsd.py
new file mode 100644
index 0000000..36f6eec
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/freebsd.py
@@ -0,0 +1,33 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
+
+
+class FreeBSDNetwork(GenericBsdIfconfigNetwork):
+ """
+ This is the FreeBSD Network Class.
+ It uses the GenericBsdIfconfigNetwork unchanged.
+ """
+ platform = 'FreeBSD'
+
+
+class FreeBSDNetworkCollector(NetworkCollector):
+ _fact_class = FreeBSDNetwork
+ _platform = 'FreeBSD'
diff --git a/lib/ansible/module_utils/facts/network/generic_bsd.py b/lib/ansible/module_utils/facts/network/generic_bsd.py
new file mode 100644
index 0000000..8d640f2
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/generic_bsd.py
@@ -0,0 +1,321 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import re
+import socket
+import struct
+
+from ansible.module_utils.facts.network.base import Network
+
+
+class GenericBsdIfconfigNetwork(Network):
+ """
+ This is a generic BSD subclass of Network using the ifconfig command.
+ It defines
+ - interfaces (a list of interface names)
+ - interface_<name> dictionary of ipv4, ipv6, and mac address information.
+ - all_ipv4_addresses and all_ipv6_addresses: lists of all configured addresses.
+ """
+ platform = 'Generic_BSD_Ifconfig'
+
+ def populate(self, collected_facts=None):
+ network_facts = {}
+ ifconfig_path = self.module.get_bin_path('ifconfig')
+
+ if ifconfig_path is None:
+ return network_facts
+
+ route_path = self.module.get_bin_path('route')
+
+ if route_path is None:
+ return network_facts
+
+ default_ipv4, default_ipv6 = self.get_default_interfaces(route_path)
+ interfaces, ips = self.get_interfaces_info(ifconfig_path)
+ interfaces = self.detect_type_media(interfaces)
+
+ self.merge_default_interface(default_ipv4, interfaces, 'ipv4')
+ self.merge_default_interface(default_ipv6, interfaces, 'ipv6')
+ network_facts['interfaces'] = sorted(list(interfaces.keys()))
+
+ for iface in interfaces:
+ network_facts[iface] = interfaces[iface]
+
+ network_facts['default_ipv4'] = default_ipv4
+ network_facts['default_ipv6'] = default_ipv6
+ network_facts['all_ipv4_addresses'] = ips['all_ipv4_addresses']
+ network_facts['all_ipv6_addresses'] = ips['all_ipv6_addresses']
+
+ return network_facts
+
+ def detect_type_media(self, interfaces):
+ for iface in interfaces:
+ if 'media' in interfaces[iface]:
+ if 'ether' in interfaces[iface]['media'].lower():
+ interfaces[iface]['type'] = 'ether'
+ return interfaces
+
+ def get_default_interfaces(self, route_path):
+
+ # Use the commands:
+ # route -n get default
+ # route -n get -inet6 default
+ # to find out the default outgoing interface, address, and gateway
+
+ command = dict(v4=[route_path, '-n', 'get', 'default'],
+ v6=[route_path, '-n', 'get', '-inet6', 'default'])
+
+ interface = dict(v4={}, v6={})
+
+ for v in 'v4', 'v6':
+
+ if v == 'v6' and not socket.has_ipv6:
+ continue
+ rc, out, err = self.module.run_command(command[v])
+ if not out:
+ # v6 routing may result in
+ # RTNETLINK answers: Invalid argument
+ continue
+ for line in out.splitlines():
+ words = line.strip().split(': ')
+ # Collect output from route command
+ if len(words) > 1:
+ if words[0] == 'interface':
+ interface[v]['interface'] = words[1]
+ if words[0] == 'gateway':
+ interface[v]['gateway'] = words[1]
+ # help pick the right interface address on OpenBSD
+ if words[0] == 'if address':
+ interface[v]['address'] = words[1]
+ # help pick the right interface address on NetBSD
+ if words[0] == 'local addr':
+ interface[v]['address'] = words[1]
+
+ return interface['v4'], interface['v6']
+
+ def get_interfaces_info(self, ifconfig_path, ifconfig_options='-a'):
+ interfaces = {}
+ current_if = {}
+ ips = dict(
+ all_ipv4_addresses=[],
+ all_ipv6_addresses=[],
+ )
+ # FreeBSD, DragonflyBSD, NetBSD, OpenBSD and macOS all implicitly add '-a'
+ # when running the command 'ifconfig'.
+ # Solaris must explicitly run the command 'ifconfig -a'.
+ rc, out, err = self.module.run_command([ifconfig_path, ifconfig_options])
+
+ for line in out.splitlines():
+
+ if line:
+ words = line.split()
+
+ if words[0] == 'pass':
+ continue
+ elif re.match(r'^\S', line) and len(words) > 3:
+ current_if = self.parse_interface_line(words)
+ interfaces[current_if['device']] = current_if
+ elif words[0].startswith('options='):
+ self.parse_options_line(words, current_if, ips)
+ elif words[0] == 'nd6':
+ self.parse_nd6_line(words, current_if, ips)
+ elif words[0] == 'ether':
+ self.parse_ether_line(words, current_if, ips)
+ elif words[0] == 'media:':
+ self.parse_media_line(words, current_if, ips)
+ elif words[0] == 'status:':
+ self.parse_status_line(words, current_if, ips)
+ elif words[0] == 'lladdr':
+ self.parse_lladdr_line(words, current_if, ips)
+ elif words[0] == 'inet':
+ self.parse_inet_line(words, current_if, ips)
+ elif words[0] == 'inet6':
+ self.parse_inet6_line(words, current_if, ips)
+ elif words[0] == 'tunnel':
+ self.parse_tunnel_line(words, current_if, ips)
+ else:
+ self.parse_unknown_line(words, current_if, ips)
+
+ return interfaces, ips
+
+ def parse_interface_line(self, words):
+ device = words[0][0:-1]
+ current_if = {'device': device, 'ipv4': [], 'ipv6': [], 'type': 'unknown'}
+ current_if['flags'] = self.get_options(words[1])
+ if 'LOOPBACK' in current_if['flags']:
+ current_if['type'] = 'loopback'
+ current_if['macaddress'] = 'unknown' # will be overwritten later
+
+ if len(words) >= 5: # Newer FreeBSD versions
+ current_if['metric'] = words[3]
+ current_if['mtu'] = words[5]
+ else:
+ current_if['mtu'] = words[3]
+
+ return current_if
+
+ def parse_options_line(self, words, current_if, ips):
+ # Mac has options like this...
+ current_if['options'] = self.get_options(words[0])
+
+ def parse_nd6_line(self, words, current_if, ips):
+ # FreeBSD has options like this...
+ current_if['options'] = self.get_options(words[1])
+
+ def parse_ether_line(self, words, current_if, ips):
+ current_if['macaddress'] = words[1]
+ current_if['type'] = 'ether'
+
+ def parse_media_line(self, words, current_if, ips):
+ # not sure if this is useful - we also drop information
+ current_if['media'] = words[1]
+ if len(words) > 2:
+ current_if['media_select'] = words[2]
+ if len(words) > 3:
+ current_if['media_type'] = words[3][1:]
+ if len(words) > 4:
+ current_if['media_options'] = self.get_options(words[4])
+
+ def parse_status_line(self, words, current_if, ips):
+ current_if['status'] = words[1]
+
+ def parse_lladdr_line(self, words, current_if, ips):
+ current_if['lladdr'] = words[1]
+
+ def parse_inet_line(self, words, current_if, ips):
+ # netbsd show aliases like this
+ # lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 33184
+ # inet 127.0.0.1 netmask 0xff000000
+ # inet alias 127.1.1.1 netmask 0xff000000
+ if words[1] == 'alias':
+ del words[1]
+
+ address = {'address': words[1]}
+ # cidr style ip address (eg, 127.0.0.1/24) in inet line
+ # used in netbsd ifconfig -e output after 7.1
+ if '/' in address['address']:
+ ip_address, cidr_mask = address['address'].split('/')
+
+ address['address'] = ip_address
+
+ netmask_length = int(cidr_mask)
+ netmask_bin = (1 << 32) - (1 << 32 >> int(netmask_length))
+ address['netmask'] = socket.inet_ntoa(struct.pack('!L', netmask_bin))
+
+ if len(words) > 5:
+ address['broadcast'] = words[3]
+
+ else:
+ # Don't just assume columns, use "netmask" as the index for the prior column
+ try:
+ netmask_idx = words.index('netmask') + 1
+ except ValueError:
+ netmask_idx = 3
+
+ # deal with hex netmask
+ if re.match('([0-9a-f]){8}$', words[netmask_idx]):
+ netmask = '0x' + words[netmask_idx]
+ else:
+ netmask = words[netmask_idx]
+
+ if netmask.startswith('0x'):
+ address['netmask'] = socket.inet_ntoa(struct.pack('!L', int(netmask, base=16)))
+ else:
+ # otherwise assume this is a dotted quad
+ address['netmask'] = netmask
+ # calculate the network
+ address_bin = struct.unpack('!L', socket.inet_aton(address['address']))[0]
+ netmask_bin = struct.unpack('!L', socket.inet_aton(address['netmask']))[0]
+ address['network'] = socket.inet_ntoa(struct.pack('!L', address_bin & netmask_bin))
+ if 'broadcast' not in address:
+ # broadcast may be given or we need to calculate
+ try:
+ broadcast_idx = words.index('broadcast') + 1
+ except ValueError:
+ address['broadcast'] = socket.inet_ntoa(struct.pack('!L', address_bin | (~netmask_bin & 0xffffffff)))
+ else:
+ address['broadcast'] = words[broadcast_idx]
+
+ # add to our list of addresses
+ if not words[1].startswith('127.'):
+ ips['all_ipv4_addresses'].append(address['address'])
+ current_if['ipv4'].append(address)
+
+ def parse_inet6_line(self, words, current_if, ips):
+ address = {'address': words[1]}
+
+ # using cidr style addresses, ala NetBSD ifconfig post 7.1
+ if '/' in address['address']:
+ ip_address, cidr_mask = address['address'].split('/')
+
+ address['address'] = ip_address
+ address['prefix'] = cidr_mask
+
+ if len(words) > 5:
+ address['scope'] = words[5]
+ else:
+ if (len(words) >= 4) and (words[2] == 'prefixlen'):
+ address['prefix'] = words[3]
+ if (len(words) >= 6) and (words[4] == 'scopeid'):
+ address['scope'] = words[5]
+
+ localhost6 = ['::1', '::1/128', 'fe80::1%lo0']
+ if address['address'] not in localhost6:
+ ips['all_ipv6_addresses'].append(address['address'])
+ current_if['ipv6'].append(address)
+
+ def parse_tunnel_line(self, words, current_if, ips):
+ current_if['type'] = 'tunnel'
+
+ def parse_unknown_line(self, words, current_if, ips):
+ # we are going to ignore unknown lines here - this may be
+ # a bad idea - but you can override it in your subclass
+ pass
+
+ # TODO: these are module scope static function candidates
+ # (most of the class is really...)
+ def get_options(self, option_string):
+ start = option_string.find('<') + 1
+ end = option_string.rfind('>')
+ if (start > 0) and (end > 0) and (end > start + 1):
+ option_csv = option_string[start:end]
+ return option_csv.split(',')
+ else:
+ return []
+
+ def merge_default_interface(self, defaults, interfaces, ip_type):
+ if 'interface' not in defaults:
+ return
+ if not defaults['interface'] in interfaces:
+ return
+ ifinfo = interfaces[defaults['interface']]
+ # copy all the interface values across except addresses
+ for item in ifinfo:
+ if item != 'ipv4' and item != 'ipv6':
+ defaults[item] = ifinfo[item]
+
+ ipinfo = []
+ if 'address' in defaults:
+ ipinfo = [x for x in ifinfo[ip_type] if x['address'] == defaults['address']]
+
+ if len(ipinfo) == 0:
+ ipinfo = ifinfo[ip_type]
+
+ if len(ipinfo) > 0:
+ for item in ipinfo[0]:
+ defaults[item] = ipinfo[0][item]
diff --git a/lib/ansible/module_utils/facts/network/hpux.py b/lib/ansible/module_utils/facts/network/hpux.py
new file mode 100644
index 0000000..add57be
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/hpux.py
@@ -0,0 +1,82 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils.facts.network.base import Network, NetworkCollector
+
+
+class HPUXNetwork(Network):
+ """
+ HP-UX-specifig subclass of Network. Defines networking facts:
+ - default_interface
+ - interfaces (a list of interface names)
+ - interface_<name> dictionary of ipv4 address information.
+ """
+ platform = 'HP-UX'
+
+ def populate(self, collected_facts=None):
+ network_facts = {}
+ netstat_path = self.module.get_bin_path('netstat')
+
+ if netstat_path is None:
+ return network_facts
+
+ default_interfaces_facts = self.get_default_interfaces()
+ network_facts.update(default_interfaces_facts)
+
+ interfaces = self.get_interfaces_info()
+ network_facts['interfaces'] = interfaces.keys()
+ for iface in interfaces:
+ network_facts[iface] = interfaces[iface]
+
+ return network_facts
+
+ def get_default_interfaces(self):
+ default_interfaces = {}
+ rc, out, err = self.module.run_command("/usr/bin/netstat -nr")
+ lines = out.splitlines()
+ for line in lines:
+ words = line.split()
+ if len(words) > 1:
+ if words[0] == 'default':
+ default_interfaces['default_interface'] = words[4]
+ default_interfaces['default_gateway'] = words[1]
+
+ return default_interfaces
+
+ def get_interfaces_info(self):
+ interfaces = {}
+ rc, out, err = self.module.run_command("/usr/bin/netstat -niw")
+ lines = out.splitlines()
+ for line in lines:
+ words = line.split()
+ for i in range(len(words) - 1):
+ if words[i][:3] == 'lan':
+ device = words[i]
+ interfaces[device] = {'device': device}
+ address = words[i + 3]
+ interfaces[device]['ipv4'] = {'address': address}
+ network = words[i + 2]
+ interfaces[device]['ipv4'] = {'network': network,
+ 'interface': device,
+ 'address': address}
+ return interfaces
+
+
+class HPUXNetworkCollector(NetworkCollector):
+ _fact_class = HPUXNetwork
+ _platform = 'HP-UX'
diff --git a/lib/ansible/module_utils/facts/network/hurd.py b/lib/ansible/module_utils/facts/network/hurd.py
new file mode 100644
index 0000000..518df39
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/hurd.py
@@ -0,0 +1,87 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+from ansible.module_utils.facts.network.base import Network, NetworkCollector
+
+
+class HurdPfinetNetwork(Network):
+ """
+ This is a GNU Hurd specific subclass of Network. It use fsysopts to
+ get the ip address and support only pfinet.
+ """
+ platform = 'GNU'
+ _socket_dir = '/servers/socket/'
+
+ def assign_network_facts(self, network_facts, fsysopts_path, socket_path):
+ rc, out, err = self.module.run_command([fsysopts_path, '-L', socket_path])
+ # FIXME: build up a interfaces datastructure, then assign into network_facts
+ network_facts['interfaces'] = []
+ for i in out.split():
+ if '=' in i and i.startswith('--'):
+ k, v = i.split('=', 1)
+ # remove '--'
+ k = k[2:]
+ if k == 'interface':
+ # remove /dev/ from /dev/eth0
+ v = v[5:]
+ network_facts['interfaces'].append(v)
+ network_facts[v] = {
+ 'active': True,
+ 'device': v,
+ 'ipv4': {},
+ 'ipv6': [],
+ }
+ current_if = v
+ elif k == 'address':
+ network_facts[current_if]['ipv4']['address'] = v
+ elif k == 'netmask':
+ network_facts[current_if]['ipv4']['netmask'] = v
+ elif k == 'address6':
+ address, prefix = v.split('/')
+ network_facts[current_if]['ipv6'].append({
+ 'address': address,
+ 'prefix': prefix,
+ })
+ return network_facts
+
+ def populate(self, collected_facts=None):
+ network_facts = {}
+
+ fsysopts_path = self.module.get_bin_path('fsysopts')
+ if fsysopts_path is None:
+ return network_facts
+
+ socket_path = None
+
+ for l in ('inet', 'inet6'):
+ link = os.path.join(self._socket_dir, l)
+ if os.path.exists(link):
+ socket_path = link
+ break
+
+ if socket_path is None:
+ return network_facts
+
+ return self.assign_network_facts(network_facts, fsysopts_path, socket_path)
+
+
+class HurdNetworkCollector(NetworkCollector):
+ _platform = 'GNU'
+ _fact_class = HurdPfinetNetwork
diff --git a/lib/ansible/module_utils/facts/network/iscsi.py b/lib/ansible/module_utils/facts/network/iscsi.py
new file mode 100644
index 0000000..2bb9383
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/iscsi.py
@@ -0,0 +1,115 @@
+# iSCSI initiator related facts collection for Ansible.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+import subprocess
+
+import ansible.module_utils.compat.typing as t
+
+from ansible.module_utils.common.process import get_bin_path
+from ansible.module_utils.facts.utils import get_file_content
+from ansible.module_utils.facts.network.base import NetworkCollector
+
+
+class IscsiInitiatorNetworkCollector(NetworkCollector):
+ name = 'iscsi'
+ _fact_ids = set() # type: t.Set[str]
+
+ def collect(self, module=None, collected_facts=None):
+ """
+ Example of contents of /etc/iscsi/initiatorname.iscsi:
+
+ ## DO NOT EDIT OR REMOVE THIS FILE!
+ ## If you remove this file, the iSCSI daemon will not start.
+ ## If you change the InitiatorName, existing access control lists
+ ## may reject this initiator. The InitiatorName must be unique
+ ## for each iSCSI initiator. Do NOT duplicate iSCSI InitiatorNames.
+ InitiatorName=iqn.1993-08.org.debian:01:44a42c8ddb8b
+
+ Example of output from the AIX lsattr command:
+
+ # lsattr -E -l iscsi0
+ disc_filename /etc/iscsi/targets Configuration file False
+ disc_policy file Discovery Policy True
+ initiator_name iqn.localhost.hostid.7f000002 iSCSI Initiator Name True
+ isns_srvnames auto iSNS Servers IP Addresses True
+ isns_srvports iSNS Servers Port Numbers True
+ max_targets 16 Maximum Targets Allowed True
+ num_cmd_elems 200 Maximum number of commands to queue to driver True
+
+ Example of output from the HP-UX iscsiutil command:
+
+ #iscsiutil -l
+ Initiator Name : iqn.1986-03.com.hp:mcel_VMhost3.1f355cf6-e2db-11e0-a999-b44c0aef5537
+ Initiator Alias :
+
+ Authentication Method : None
+ CHAP Method : CHAP_UNI
+ Initiator CHAP Name :
+ CHAP Secret :
+ NAS Hostname :
+ NAS Secret :
+ Radius Server Hostname :
+ Header Digest : None, CRC32C (default)
+ Data Digest : None, CRC32C (default)
+ SLP Scope list for iSLPD :
+ """
+
+ iscsi_facts = {}
+ iscsi_facts['iscsi_iqn'] = ""
+ if sys.platform.startswith('linux') or sys.platform.startswith('sunos'):
+ for line in get_file_content('/etc/iscsi/initiatorname.iscsi', '').splitlines():
+ if line.startswith('#') or line.startswith(';') or line.strip() == '':
+ continue
+ if line.startswith('InitiatorName='):
+ iscsi_facts['iscsi_iqn'] = line.split('=', 1)[1]
+ break
+ elif sys.platform.startswith('aix'):
+ try:
+ cmd = get_bin_path('lsattr')
+ except ValueError:
+ return iscsi_facts
+
+ cmd += " -E -l iscsi0"
+ rc, out, err = module.run_command(cmd)
+ if rc == 0 and out:
+ line = self.findstr(out, 'initiator_name')
+ iscsi_facts['iscsi_iqn'] = line.split()[1].rstrip()
+
+ elif sys.platform.startswith('hp-ux'):
+ # try to find it in the default PATH and opt_dirs
+ try:
+ cmd = get_bin_path('iscsiutil', opt_dirs=['/opt/iscsi/bin'])
+ except ValueError:
+ return iscsi_facts
+
+ cmd += " -l"
+ rc, out, err = module.run_command(cmd)
+ if out:
+ line = self.findstr(out, 'Initiator Name')
+ iscsi_facts['iscsi_iqn'] = line.split(":", 1)[1].rstrip()
+
+ return iscsi_facts
+
+ def findstr(self, text, match):
+ for line in text.splitlines():
+ if match in line:
+ found = line
+ return found
diff --git a/lib/ansible/module_utils/facts/network/linux.py b/lib/ansible/module_utils/facts/network/linux.py
new file mode 100644
index 0000000..b7ae976
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/linux.py
@@ -0,0 +1,327 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import glob
+import os
+import re
+import socket
+import struct
+
+from ansible.module_utils.facts.network.base import Network, NetworkCollector
+
+from ansible.module_utils.facts.utils import get_file_content
+
+
+class LinuxNetwork(Network):
+ """
+ This is a Linux-specific subclass of Network. It defines
+ - interfaces (a list of interface names)
+ - interface_<name> dictionary of ipv4, ipv6, and mac address information.
+ - all_ipv4_addresses and all_ipv6_addresses: lists of all configured addresses.
+ - ipv4_address and ipv6_address: the first non-local address for each family.
+ """
+ platform = 'Linux'
+ INTERFACE_TYPE = {
+ '1': 'ether',
+ '32': 'infiniband',
+ '512': 'ppp',
+ '772': 'loopback',
+ '65534': 'tunnel',
+ }
+
+ def populate(self, collected_facts=None):
+ network_facts = {}
+ ip_path = self.module.get_bin_path('ip')
+ if ip_path is None:
+ return network_facts
+ default_ipv4, default_ipv6 = self.get_default_interfaces(ip_path,
+ collected_facts=collected_facts)
+ interfaces, ips = self.get_interfaces_info(ip_path, default_ipv4, default_ipv6)
+ network_facts['interfaces'] = interfaces.keys()
+ for iface in interfaces:
+ network_facts[iface] = interfaces[iface]
+ network_facts['default_ipv4'] = default_ipv4
+ network_facts['default_ipv6'] = default_ipv6
+ network_facts['all_ipv4_addresses'] = ips['all_ipv4_addresses']
+ network_facts['all_ipv6_addresses'] = ips['all_ipv6_addresses']
+ return network_facts
+
+ def get_default_interfaces(self, ip_path, collected_facts=None):
+ collected_facts = collected_facts or {}
+ # Use the commands:
+ # ip -4 route get 8.8.8.8 -> Google public DNS
+ # ip -6 route get 2404:6800:400a:800::1012 -> ipv6.google.com
+ # to find out the default outgoing interface, address, and gateway
+ command = dict(
+ v4=[ip_path, '-4', 'route', 'get', '8.8.8.8'],
+ v6=[ip_path, '-6', 'route', 'get', '2404:6800:400a:800::1012']
+ )
+ interface = dict(v4={}, v6={})
+
+ for v in 'v4', 'v6':
+ if (v == 'v6' and collected_facts.get('ansible_os_family') == 'RedHat' and
+ collected_facts.get('ansible_distribution_version', '').startswith('4.')):
+ continue
+ if v == 'v6' and not socket.has_ipv6:
+ continue
+ rc, out, err = self.module.run_command(command[v], errors='surrogate_then_replace')
+ if not out:
+ # v6 routing may result in
+ # RTNETLINK answers: Invalid argument
+ continue
+ words = out.splitlines()[0].split()
+ # A valid output starts with the queried address on the first line
+ if len(words) > 0 and words[0] == command[v][-1]:
+ for i in range(len(words) - 1):
+ if words[i] == 'dev':
+ interface[v]['interface'] = words[i + 1]
+ elif words[i] == 'src':
+ interface[v]['address'] = words[i + 1]
+ elif words[i] == 'via' and words[i + 1] != command[v][-1]:
+ interface[v]['gateway'] = words[i + 1]
+ return interface['v4'], interface['v6']
+
+ def get_interfaces_info(self, ip_path, default_ipv4, default_ipv6):
+ interfaces = {}
+ ips = dict(
+ all_ipv4_addresses=[],
+ all_ipv6_addresses=[],
+ )
+
+ # FIXME: maybe split into smaller methods?
+ # FIXME: this is pretty much a constructor
+
+ for path in glob.glob('/sys/class/net/*'):
+ if not os.path.isdir(path):
+ continue
+ device = os.path.basename(path)
+ interfaces[device] = {'device': device}
+ if os.path.exists(os.path.join(path, 'address')):
+ macaddress = get_file_content(os.path.join(path, 'address'), default='')
+ if macaddress and macaddress != '00:00:00:00:00:00':
+ interfaces[device]['macaddress'] = macaddress
+ if os.path.exists(os.path.join(path, 'mtu')):
+ interfaces[device]['mtu'] = int(get_file_content(os.path.join(path, 'mtu')))
+ if os.path.exists(os.path.join(path, 'operstate')):
+ interfaces[device]['active'] = get_file_content(os.path.join(path, 'operstate')) != 'down'
+ if os.path.exists(os.path.join(path, 'device', 'driver', 'module')):
+ interfaces[device]['module'] = os.path.basename(os.path.realpath(os.path.join(path, 'device', 'driver', 'module')))
+ if os.path.exists(os.path.join(path, 'type')):
+ _type = get_file_content(os.path.join(path, 'type'))
+ interfaces[device]['type'] = self.INTERFACE_TYPE.get(_type, 'unknown')
+ if os.path.exists(os.path.join(path, 'bridge')):
+ interfaces[device]['type'] = 'bridge'
+ interfaces[device]['interfaces'] = [os.path.basename(b) for b in glob.glob(os.path.join(path, 'brif', '*'))]
+ if os.path.exists(os.path.join(path, 'bridge', 'bridge_id')):
+ interfaces[device]['id'] = get_file_content(os.path.join(path, 'bridge', 'bridge_id'), default='')
+ if os.path.exists(os.path.join(path, 'bridge', 'stp_state')):
+ interfaces[device]['stp'] = get_file_content(os.path.join(path, 'bridge', 'stp_state')) == '1'
+ if os.path.exists(os.path.join(path, 'bonding')):
+ interfaces[device]['type'] = 'bonding'
+ interfaces[device]['slaves'] = get_file_content(os.path.join(path, 'bonding', 'slaves'), default='').split()
+ interfaces[device]['mode'] = get_file_content(os.path.join(path, 'bonding', 'mode'), default='').split()[0]
+ interfaces[device]['miimon'] = get_file_content(os.path.join(path, 'bonding', 'miimon'), default='').split()[0]
+ interfaces[device]['lacp_rate'] = get_file_content(os.path.join(path, 'bonding', 'lacp_rate'), default='').split()[0]
+ primary = get_file_content(os.path.join(path, 'bonding', 'primary'))
+ if primary:
+ interfaces[device]['primary'] = primary
+ path = os.path.join(path, 'bonding', 'all_slaves_active')
+ if os.path.exists(path):
+ interfaces[device]['all_slaves_active'] = get_file_content(path) == '1'
+ if os.path.exists(os.path.join(path, 'bonding_slave')):
+ interfaces[device]['perm_macaddress'] = get_file_content(os.path.join(path, 'bonding_slave', 'perm_hwaddr'), default='')
+ if os.path.exists(os.path.join(path, 'device')):
+ interfaces[device]['pciid'] = os.path.basename(os.readlink(os.path.join(path, 'device')))
+ if os.path.exists(os.path.join(path, 'speed')):
+ speed = get_file_content(os.path.join(path, 'speed'))
+ if speed is not None:
+ interfaces[device]['speed'] = int(speed)
+
+ # Check whether an interface is in promiscuous mode
+ if os.path.exists(os.path.join(path, 'flags')):
+ promisc_mode = False
+ # The second byte indicates whether the interface is in promiscuous mode.
+ # 1 = promisc
+ # 0 = no promisc
+ data = int(get_file_content(os.path.join(path, 'flags')), 16)
+ promisc_mode = (data & 0x0100 > 0)
+ interfaces[device]['promisc'] = promisc_mode
+
+ # TODO: determine if this needs to be in a nested scope/closure
+ def parse_ip_output(output, secondary=False):
+ for line in output.splitlines():
+ if not line:
+ continue
+ words = line.split()
+ broadcast = ''
+ if words[0] == 'inet':
+ if '/' in words[1]:
+ address, netmask_length = words[1].split('/')
+ if len(words) > 3:
+ if words[2] == 'brd':
+ broadcast = words[3]
+ else:
+ # pointopoint interfaces do not have a prefix
+ address = words[1]
+ netmask_length = "32"
+ address_bin = struct.unpack('!L', socket.inet_aton(address))[0]
+ netmask_bin = (1 << 32) - (1 << 32 >> int(netmask_length))
+ netmask = socket.inet_ntoa(struct.pack('!L', netmask_bin))
+ network = socket.inet_ntoa(struct.pack('!L', address_bin & netmask_bin))
+ iface = words[-1]
+ # NOTE: device is ref to outside scope
+ # NOTE: interfaces is also ref to outside scope
+ if iface != device:
+ interfaces[iface] = {}
+ if not secondary and "ipv4" not in interfaces[iface]:
+ interfaces[iface]['ipv4'] = {'address': address,
+ 'broadcast': broadcast,
+ 'netmask': netmask,
+ 'network': network,
+ 'prefix': netmask_length,
+ }
+ else:
+ if "ipv4_secondaries" not in interfaces[iface]:
+ interfaces[iface]["ipv4_secondaries"] = []
+ interfaces[iface]["ipv4_secondaries"].append({
+ 'address': address,
+ 'broadcast': broadcast,
+ 'netmask': netmask,
+ 'network': network,
+ 'prefix': netmask_length,
+ })
+
+ # add this secondary IP to the main device
+ if secondary:
+ if "ipv4_secondaries" not in interfaces[device]:
+ interfaces[device]["ipv4_secondaries"] = []
+ if device != iface:
+ interfaces[device]["ipv4_secondaries"].append({
+ 'address': address,
+ 'broadcast': broadcast,
+ 'netmask': netmask,
+ 'network': network,
+ 'prefix': netmask_length,
+ })
+
+ # NOTE: default_ipv4 is ref to outside scope
+ # If this is the default address, update default_ipv4
+ if 'address' in default_ipv4 and default_ipv4['address'] == address:
+ default_ipv4['broadcast'] = broadcast
+ default_ipv4['netmask'] = netmask
+ default_ipv4['network'] = network
+ default_ipv4['prefix'] = netmask_length
+ # NOTE: macaddress is ref from outside scope
+ default_ipv4['macaddress'] = macaddress
+ default_ipv4['mtu'] = interfaces[device]['mtu']
+ default_ipv4['type'] = interfaces[device].get("type", "unknown")
+ default_ipv4['alias'] = words[-1]
+ if not address.startswith('127.'):
+ ips['all_ipv4_addresses'].append(address)
+ elif words[0] == 'inet6':
+ if 'peer' == words[2]:
+ address = words[1]
+ _, prefix = words[3].split('/')
+ scope = words[5]
+ else:
+ address, prefix = words[1].split('/')
+ scope = words[3]
+ if 'ipv6' not in interfaces[device]:
+ interfaces[device]['ipv6'] = []
+ interfaces[device]['ipv6'].append({
+ 'address': address,
+ 'prefix': prefix,
+ 'scope': scope
+ })
+ # If this is the default address, update default_ipv6
+ if 'address' in default_ipv6 and default_ipv6['address'] == address:
+ default_ipv6['prefix'] = prefix
+ default_ipv6['scope'] = scope
+ default_ipv6['macaddress'] = macaddress
+ default_ipv6['mtu'] = interfaces[device]['mtu']
+ default_ipv6['type'] = interfaces[device].get("type", "unknown")
+ if not address == '::1':
+ ips['all_ipv6_addresses'].append(address)
+
+ ip_path = self.module.get_bin_path("ip")
+
+ args = [ip_path, 'addr', 'show', 'primary', 'dev', device]
+ rc, primary_data, stderr = self.module.run_command(args, errors='surrogate_then_replace')
+ if rc == 0:
+ parse_ip_output(primary_data)
+ else:
+ # possibly busybox, fallback to running without the "primary" arg
+ # https://github.com/ansible/ansible/issues/50871
+ args = [ip_path, 'addr', 'show', 'dev', device]
+ rc, data, stderr = self.module.run_command(args, errors='surrogate_then_replace')
+ if rc == 0:
+ parse_ip_output(data)
+
+ args = [ip_path, 'addr', 'show', 'secondary', 'dev', device]
+ rc, secondary_data, stderr = self.module.run_command(args, errors='surrogate_then_replace')
+ if rc == 0:
+ parse_ip_output(secondary_data, secondary=True)
+
+ interfaces[device].update(self.get_ethtool_data(device))
+
+ # replace : by _ in interface name since they are hard to use in template
+ new_interfaces = {}
+ # i is a dict key (string) not an index int
+ for i in interfaces:
+ if ':' in i:
+ new_interfaces[i.replace(':', '_')] = interfaces[i]
+ else:
+ new_interfaces[i] = interfaces[i]
+ return new_interfaces, ips
+
+ def get_ethtool_data(self, device):
+
+ data = {}
+ ethtool_path = self.module.get_bin_path("ethtool")
+ # FIXME: exit early on falsey ethtool_path and un-indent
+ if ethtool_path:
+ args = [ethtool_path, '-k', device]
+ rc, stdout, stderr = self.module.run_command(args, errors='surrogate_then_replace')
+ # FIXME: exit early on falsey if we can
+ if rc == 0:
+ features = {}
+ for line in stdout.strip().splitlines():
+ if not line or line.endswith(":"):
+ continue
+ key, value = line.split(": ")
+ if not value:
+ continue
+ features[key.strip().replace('-', '_')] = value.strip()
+ data['features'] = features
+
+ args = [ethtool_path, '-T', device]
+ rc, stdout, stderr = self.module.run_command(args, errors='surrogate_then_replace')
+ if rc == 0:
+ data['timestamping'] = [m.lower() for m in re.findall(r'SOF_TIMESTAMPING_(\w+)', stdout)]
+ data['hw_timestamp_filters'] = [m.lower() for m in re.findall(r'HWTSTAMP_FILTER_(\w+)', stdout)]
+ m = re.search(r'PTP Hardware Clock: (\d+)', stdout)
+ if m:
+ data['phc_index'] = int(m.groups()[0])
+
+ return data
+
+
+class LinuxNetworkCollector(NetworkCollector):
+ _platform = 'Linux'
+ _fact_class = LinuxNetwork
+ required_facts = set(['distribution', 'platform'])
diff --git a/lib/ansible/module_utils/facts/network/netbsd.py b/lib/ansible/module_utils/facts/network/netbsd.py
new file mode 100644
index 0000000..de8ceff
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/netbsd.py
@@ -0,0 +1,48 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
+
+
+class NetBSDNetwork(GenericBsdIfconfigNetwork):
+ """
+ This is the NetBSD Network Class.
+ It uses the GenericBsdIfconfigNetwork
+ """
+ platform = 'NetBSD'
+
+ def parse_media_line(self, words, current_if, ips):
+ # example of line:
+ # $ ifconfig
+ # ne0: flags=8863<UP,BROADCAST,NOTRAILERS,RUNNING,SIMPLEX,MULTICAST> mtu 1500
+ # ec_capabilities=1<VLAN_MTU>
+ # ec_enabled=0
+ # address: 00:20:91:45:00:78
+ # media: Ethernet 10baseT full-duplex
+ # inet 192.168.156.29 netmask 0xffffff00 broadcast 192.168.156.255
+ current_if['media'] = words[1]
+ if len(words) > 2:
+ current_if['media_type'] = words[2]
+ if len(words) > 3:
+ current_if['media_options'] = words[3].split(',')
+
+
+class NetBSDNetworkCollector(NetworkCollector):
+ _fact_class = NetBSDNetwork
+ _platform = 'NetBSD'
diff --git a/lib/ansible/module_utils/facts/network/nvme.py b/lib/ansible/module_utils/facts/network/nvme.py
new file mode 100644
index 0000000..febd0ab
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/nvme.py
@@ -0,0 +1,57 @@
+# NVMe initiator related facts collection for Ansible.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+import subprocess
+
+import ansible.module_utils.compat.typing as t
+
+from ansible.module_utils.facts.utils import get_file_content
+from ansible.module_utils.facts.network.base import NetworkCollector
+
+
+class NvmeInitiatorNetworkCollector(NetworkCollector):
+ name = 'nvme'
+ _fact_ids = set() # type: t.Set[str]
+
+ def collect(self, module=None, collected_facts=None):
+ """
+ Currently NVMe is only supported in some Linux distributions.
+ If NVMe is configured on the host then a file will have been created
+ during the NVMe driver installation. This file holds the unique NQN
+ of the host.
+
+ Example of contents of /etc/nvme/hostnqn:
+
+ # cat /etc/nvme/hostnqn
+ nqn.2014-08.org.nvmexpress:fc_lif:uuid:2cd61a74-17f9-4c22-b350-3020020c458d
+
+ """
+
+ nvme_facts = {}
+ nvme_facts['hostnqn'] = ""
+ if sys.platform.startswith('linux'):
+ for line in get_file_content('/etc/nvme/hostnqn', '').splitlines():
+ if line.startswith('#') or line.startswith(';') or line.strip() == '':
+ continue
+ if line.startswith('nqn.'):
+ nvme_facts['hostnqn'] = line
+ break
+ return nvme_facts
diff --git a/lib/ansible/module_utils/facts/network/openbsd.py b/lib/ansible/module_utils/facts/network/openbsd.py
new file mode 100644
index 0000000..9e11d82
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/openbsd.py
@@ -0,0 +1,42 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
+
+
+class OpenBSDNetwork(GenericBsdIfconfigNetwork):
+ """
+ This is the OpenBSD Network Class.
+ It uses the GenericBsdIfconfigNetwork.
+ """
+ platform = 'OpenBSD'
+
+ # OpenBSD 'ifconfig -a' does not have information about aliases
+ def get_interfaces_info(self, ifconfig_path, ifconfig_options='-aA'):
+ return super(OpenBSDNetwork, self).get_interfaces_info(ifconfig_path, ifconfig_options)
+
+ # Return macaddress instead of lladdr
+ def parse_lladdr_line(self, words, current_if, ips):
+ current_if['macaddress'] = words[1]
+ current_if['type'] = 'ether'
+
+
+class OpenBSDNetworkCollector(NetworkCollector):
+ _fact_class = OpenBSDNetwork
+ _platform = 'OpenBSD'
diff --git a/lib/ansible/module_utils/facts/network/sunos.py b/lib/ansible/module_utils/facts/network/sunos.py
new file mode 100644
index 0000000..adba14c
--- /dev/null
+++ b/lib/ansible/module_utils/facts/network/sunos.py
@@ -0,0 +1,116 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import re
+
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.network.generic_bsd import GenericBsdIfconfigNetwork
+
+
+class SunOSNetwork(GenericBsdIfconfigNetwork):
+ """
+ This is the SunOS Network Class.
+ It uses the GenericBsdIfconfigNetwork.
+
+ Solaris can have different FLAGS and MTU for IPv4 and IPv6 on the same interface
+ so these facts have been moved inside the 'ipv4' and 'ipv6' lists.
+ """
+ platform = 'SunOS'
+
+ # Solaris 'ifconfig -a' will print interfaces twice, once for IPv4 and again for IPv6.
+ # MTU and FLAGS also may differ between IPv4 and IPv6 on the same interface.
+ # 'parse_interface_line()' checks for previously seen interfaces before defining
+ # 'current_if' so that IPv6 facts don't clobber IPv4 facts (or vice versa).
+ def get_interfaces_info(self, ifconfig_path):
+ interfaces = {}
+ current_if = {}
+ ips = dict(
+ all_ipv4_addresses=[],
+ all_ipv6_addresses=[],
+ )
+ rc, out, err = self.module.run_command([ifconfig_path, '-a'])
+
+ for line in out.splitlines():
+
+ if line:
+ words = line.split()
+
+ if re.match(r'^\S', line) and len(words) > 3:
+ current_if = self.parse_interface_line(words, current_if, interfaces)
+ interfaces[current_if['device']] = current_if
+ elif words[0].startswith('options='):
+ self.parse_options_line(words, current_if, ips)
+ elif words[0] == 'nd6':
+ self.parse_nd6_line(words, current_if, ips)
+ elif words[0] == 'ether':
+ self.parse_ether_line(words, current_if, ips)
+ elif words[0] == 'media:':
+ self.parse_media_line(words, current_if, ips)
+ elif words[0] == 'status:':
+ self.parse_status_line(words, current_if, ips)
+ elif words[0] == 'lladdr':
+ self.parse_lladdr_line(words, current_if, ips)
+ elif words[0] == 'inet':
+ self.parse_inet_line(words, current_if, ips)
+ elif words[0] == 'inet6':
+ self.parse_inet6_line(words, current_if, ips)
+ else:
+ self.parse_unknown_line(words, current_if, ips)
+
+ # 'parse_interface_line' and 'parse_inet*_line' leave two dicts in the
+ # ipv4/ipv6 lists which is ugly and hard to read.
+ # This quick hack merges the dictionaries. Purely cosmetic.
+ for iface in interfaces:
+ for v in 'ipv4', 'ipv6':
+ combined_facts = {}
+ for facts in interfaces[iface][v]:
+ combined_facts.update(facts)
+ if len(combined_facts.keys()) > 0:
+ interfaces[iface][v] = [combined_facts]
+
+ return interfaces, ips
+
+ def parse_interface_line(self, words, current_if, interfaces):
+ device = words[0][0:-1]
+ if device not in interfaces:
+ current_if = {'device': device, 'ipv4': [], 'ipv6': [], 'type': 'unknown'}
+ else:
+ current_if = interfaces[device]
+ flags = self.get_options(words[1])
+ v = 'ipv4'
+ if 'IPv6' in flags:
+ v = 'ipv6'
+ if 'LOOPBACK' in flags:
+ current_if['type'] = 'loopback'
+ current_if[v].append({'flags': flags, 'mtu': words[3]})
+ current_if['macaddress'] = 'unknown' # will be overwritten later
+ return current_if
+
+ # Solaris displays single digit octets in MAC addresses e.g. 0:1:2:d:e:f
+ # Add leading zero to each octet where needed.
+ def parse_ether_line(self, words, current_if, ips):
+ macaddress = ''
+ for octet in words[1].split(':'):
+ octet = ('0' + octet)[-2:None]
+ macaddress += (octet + ':')
+ current_if['macaddress'] = macaddress[0:-1]
+
+
+class SunOSNetworkCollector(NetworkCollector):
+ _fact_class = SunOSNetwork
+ _platform = 'SunOS'