1# This file is part of Ansible 2# 3# Ansible is free software: you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation, either version 3 of the License, or 6# (at your option) any later version. 7# 8# Ansible is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 15 16from __future__ import (absolute_import, division, print_function) 17__metaclass__ = type 18 19import glob 20import os 21import re 22import socket 23import struct 24 25from ansible.module_utils.facts.network.base import Network, NetworkCollector 26 27from ansible.module_utils.facts.utils import get_file_content 28 29 30class LinuxNetwork(Network): 31 """ 32 This is a Linux-specific subclass of Network. It defines 33 - interfaces (a list of interface names) 34 - interface_<name> dictionary of ipv4, ipv6, and mac address information. 35 - all_ipv4_addresses and all_ipv6_addresses: lists of all configured addresses. 36 - ipv4_address and ipv6_address: the first non-local address for each family. 37 """ 38 platform = 'Linux' 39 INTERFACE_TYPE = { 40 '1': 'ether', 41 '32': 'infiniband', 42 '512': 'ppp', 43 '772': 'loopback', 44 '65534': 'tunnel', 45 } 46 47 def populate(self, collected_facts=None): 48 network_facts = {} 49 ip_path = self.module.get_bin_path('ip') 50 if ip_path is None: 51 return network_facts 52 default_ipv4, default_ipv6 = self.get_default_interfaces(ip_path, 53 collected_facts=collected_facts) 54 interfaces, ips = self.get_interfaces_info(ip_path, default_ipv4, default_ipv6) 55 network_facts['interfaces'] = interfaces.keys() 56 for iface in interfaces: 57 network_facts[iface] = interfaces[iface] 58 network_facts['default_ipv4'] = default_ipv4 59 network_facts['default_ipv6'] = default_ipv6 60 network_facts['all_ipv4_addresses'] = ips['all_ipv4_addresses'] 61 network_facts['all_ipv6_addresses'] = ips['all_ipv6_addresses'] 62 return network_facts 63 64 def get_default_interfaces(self, ip_path, collected_facts=None): 65 collected_facts = collected_facts or {} 66 # Use the commands: 67 # ip -4 route get 8.8.8.8 -> Google public DNS 68 # ip -6 route get 2404:6800:400a:800::1012 -> ipv6.google.com 69 # to find out the default outgoing interface, address, and gateway 70 command = dict( 71 v4=[ip_path, '-4', 'route', 'get', '8.8.8.8'], 72 v6=[ip_path, '-6', 'route', 'get', '2404:6800:400a:800::1012'] 73 ) 74 interface = dict(v4={}, v6={}) 75 76 for v in 'v4', 'v6': 77 if (v == 'v6' and collected_facts.get('ansible_os_family') == 'RedHat' and 78 collected_facts.get('ansible_distribution_version', '').startswith('4.')): 79 continue 80 if v == 'v6' and not socket.has_ipv6: 81 continue 82 rc, out, err = self.module.run_command(command[v], errors='surrogate_then_replace') 83 if not out: 84 # v6 routing may result in 85 # RTNETLINK answers: Invalid argument 86 continue 87 words = out.splitlines()[0].split() 88 # A valid output starts with the queried address on the first line 89 if len(words) > 0 and words[0] == command[v][-1]: 90 for i in range(len(words) - 1): 91 if words[i] == 'dev': 92 interface[v]['interface'] = words[i + 1] 93 elif words[i] == 'src': 94 interface[v]['address'] = words[i + 1] 95 elif words[i] == 'via' and words[i + 1] != command[v][-1]: 96 interface[v]['gateway'] = words[i + 1] 97 return interface['v4'], interface['v6'] 98 99 def get_interfaces_info(self, ip_path, default_ipv4, default_ipv6): 100 interfaces = {} 101 ips = dict( 102 all_ipv4_addresses=[], 103 all_ipv6_addresses=[], 104 ) 105 106 # FIXME: maybe split into smaller methods? 107 # FIXME: this is pretty much a constructor 108 109 for path in glob.glob('/sys/class/net/*'): 110 if not os.path.isdir(path): 111 continue 112 device = os.path.basename(path) 113 interfaces[device] = {'device': device} 114 if os.path.exists(os.path.join(path, 'address')): 115 macaddress = get_file_content(os.path.join(path, 'address'), default='') 116 if macaddress and macaddress != '00:00:00:00:00:00': 117 interfaces[device]['macaddress'] = macaddress 118 if os.path.exists(os.path.join(path, 'mtu')): 119 interfaces[device]['mtu'] = int(get_file_content(os.path.join(path, 'mtu'))) 120 if os.path.exists(os.path.join(path, 'operstate')): 121 interfaces[device]['active'] = get_file_content(os.path.join(path, 'operstate')) != 'down' 122 if os.path.exists(os.path.join(path, 'device', 'driver', 'module')): 123 interfaces[device]['module'] = os.path.basename(os.path.realpath(os.path.join(path, 'device', 'driver', 'module'))) 124 if os.path.exists(os.path.join(path, 'type')): 125 _type = get_file_content(os.path.join(path, 'type')) 126 interfaces[device]['type'] = self.INTERFACE_TYPE.get(_type, 'unknown') 127 if os.path.exists(os.path.join(path, 'bridge')): 128 interfaces[device]['type'] = 'bridge' 129 interfaces[device]['interfaces'] = [os.path.basename(b) for b in glob.glob(os.path.join(path, 'brif', '*'))] 130 if os.path.exists(os.path.join(path, 'bridge', 'bridge_id')): 131 interfaces[device]['id'] = get_file_content(os.path.join(path, 'bridge', 'bridge_id'), default='') 132 if os.path.exists(os.path.join(path, 'bridge', 'stp_state')): 133 interfaces[device]['stp'] = get_file_content(os.path.join(path, 'bridge', 'stp_state')) == '1' 134 if os.path.exists(os.path.join(path, 'bonding')): 135 interfaces[device]['type'] = 'bonding' 136 interfaces[device]['slaves'] = get_file_content(os.path.join(path, 'bonding', 'slaves'), default='').split() 137 interfaces[device]['mode'] = get_file_content(os.path.join(path, 'bonding', 'mode'), default='').split()[0] 138 interfaces[device]['miimon'] = get_file_content(os.path.join(path, 'bonding', 'miimon'), default='').split()[0] 139 interfaces[device]['lacp_rate'] = get_file_content(os.path.join(path, 'bonding', 'lacp_rate'), default='').split()[0] 140 primary = get_file_content(os.path.join(path, 'bonding', 'primary')) 141 if primary: 142 interfaces[device]['primary'] = primary 143 path = os.path.join(path, 'bonding', 'all_slaves_active') 144 if os.path.exists(path): 145 interfaces[device]['all_slaves_active'] = get_file_content(path) == '1' 146 if os.path.exists(os.path.join(path, 'bonding_slave')): 147 interfaces[device]['perm_macaddress'] = get_file_content(os.path.join(path, 'bonding_slave', 'perm_hwaddr'), default='') 148 if os.path.exists(os.path.join(path, 'device')): 149 interfaces[device]['pciid'] = os.path.basename(os.readlink(os.path.join(path, 'device'))) 150 if os.path.exists(os.path.join(path, 'speed')): 151 speed = get_file_content(os.path.join(path, 'speed')) 152 if speed is not None: 153 interfaces[device]['speed'] = int(speed) 154 155 # Check whether an interface is in promiscuous mode 156 if os.path.exists(os.path.join(path, 'flags')): 157 promisc_mode = False 158 # The second byte indicates whether the interface is in promiscuous mode. 159 # 1 = promisc 160 # 0 = no promisc 161 data = int(get_file_content(os.path.join(path, 'flags')), 16) 162 promisc_mode = (data & 0x0100 > 0) 163 interfaces[device]['promisc'] = promisc_mode 164 165 # TODO: determine if this needs to be in a nested scope/closure 166 def parse_ip_output(output, secondary=False): 167 for line in output.splitlines(): 168 if not line: 169 continue 170 words = line.split() 171 broadcast = '' 172 if words[0] == 'inet': 173 if '/' in words[1]: 174 address, netmask_length = words[1].split('/') 175 if len(words) > 3: 176 if words[2] == 'brd': 177 broadcast = words[3] 178 else: 179 # pointopoint interfaces do not have a prefix 180 address = words[1] 181 netmask_length = "32" 182 address_bin = struct.unpack('!L', socket.inet_aton(address))[0] 183 netmask_bin = (1 << 32) - (1 << 32 >> int(netmask_length)) 184 netmask = socket.inet_ntoa(struct.pack('!L', netmask_bin)) 185 network = socket.inet_ntoa(struct.pack('!L', address_bin & netmask_bin)) 186 iface = words[-1] 187 # NOTE: device is ref to outside scope 188 # NOTE: interfaces is also ref to outside scope 189 if iface != device: 190 interfaces[iface] = {} 191 if not secondary and "ipv4" not in interfaces[iface]: 192 interfaces[iface]['ipv4'] = {'address': address, 193 'broadcast': broadcast, 194 'netmask': netmask, 195 'network': network} 196 else: 197 if "ipv4_secondaries" not in interfaces[iface]: 198 interfaces[iface]["ipv4_secondaries"] = [] 199 interfaces[iface]["ipv4_secondaries"].append({ 200 'address': address, 201 'broadcast': broadcast, 202 'netmask': netmask, 203 'network': network, 204 }) 205 206 # add this secondary IP to the main device 207 if secondary: 208 if "ipv4_secondaries" not in interfaces[device]: 209 interfaces[device]["ipv4_secondaries"] = [] 210 if device != iface: 211 interfaces[device]["ipv4_secondaries"].append({ 212 'address': address, 213 'broadcast': broadcast, 214 'netmask': netmask, 215 'network': network, 216 }) 217 218 # NOTE: default_ipv4 is ref to outside scope 219 # If this is the default address, update default_ipv4 220 if 'address' in default_ipv4 and default_ipv4['address'] == address: 221 default_ipv4['broadcast'] = broadcast 222 default_ipv4['netmask'] = netmask 223 default_ipv4['network'] = network 224 # NOTE: macaddress is ref from outside scope 225 default_ipv4['macaddress'] = macaddress 226 default_ipv4['mtu'] = interfaces[device]['mtu'] 227 default_ipv4['type'] = interfaces[device].get("type", "unknown") 228 default_ipv4['alias'] = words[-1] 229 if not address.startswith('127.'): 230 ips['all_ipv4_addresses'].append(address) 231 elif words[0] == 'inet6': 232 if 'peer' == words[2]: 233 address = words[1] 234 _, prefix = words[3].split('/') 235 scope = words[5] 236 else: 237 address, prefix = words[1].split('/') 238 scope = words[3] 239 if 'ipv6' not in interfaces[device]: 240 interfaces[device]['ipv6'] = [] 241 interfaces[device]['ipv6'].append({ 242 'address': address, 243 'prefix': prefix, 244 'scope': scope 245 }) 246 # If this is the default address, update default_ipv6 247 if 'address' in default_ipv6 and default_ipv6['address'] == address: 248 default_ipv6['prefix'] = prefix 249 default_ipv6['scope'] = scope 250 default_ipv6['macaddress'] = macaddress 251 default_ipv6['mtu'] = interfaces[device]['mtu'] 252 default_ipv6['type'] = interfaces[device].get("type", "unknown") 253 if not address == '::1': 254 ips['all_ipv6_addresses'].append(address) 255 256 ip_path = self.module.get_bin_path("ip") 257 258 args = [ip_path, 'addr', 'show', 'primary', device] 259 rc, primary_data, stderr = self.module.run_command(args, errors='surrogate_then_replace') 260 if rc == 0: 261 parse_ip_output(primary_data) 262 else: 263 # possibly busybox, fallback to running without the "primary" arg 264 # https://github.com/ansible/ansible/issues/50871 265 args = [ip_path, 'addr', 'show', device] 266 rc, data, stderr = self.module.run_command(args, errors='surrogate_then_replace') 267 if rc == 0: 268 parse_ip_output(data) 269 270 args = [ip_path, 'addr', 'show', 'secondary', device] 271 rc, secondary_data, stderr = self.module.run_command(args, errors='surrogate_then_replace') 272 if rc == 0: 273 parse_ip_output(secondary_data, secondary=True) 274 275 interfaces[device].update(self.get_ethtool_data(device)) 276 277 # replace : by _ in interface name since they are hard to use in template 278 new_interfaces = {} 279 # i is a dict key (string) not an index int 280 for i in interfaces: 281 if ':' in i: 282 new_interfaces[i.replace(':', '_')] = interfaces[i] 283 else: 284 new_interfaces[i] = interfaces[i] 285 return new_interfaces, ips 286 287 def get_ethtool_data(self, device): 288 289 data = {} 290 ethtool_path = self.module.get_bin_path("ethtool") 291 # FIXME: exit early on falsey ethtool_path and un-indent 292 if ethtool_path: 293 args = [ethtool_path, '-k', device] 294 rc, stdout, stderr = self.module.run_command(args, errors='surrogate_then_replace') 295 # FIXME: exit early on falsey if we can 296 if rc == 0: 297 features = {} 298 for line in stdout.strip().splitlines(): 299 if not line or line.endswith(":"): 300 continue 301 key, value = line.split(": ") 302 if not value: 303 continue 304 features[key.strip().replace('-', '_')] = value.strip() 305 data['features'] = features 306 307 args = [ethtool_path, '-T', device] 308 rc, stdout, stderr = self.module.run_command(args, errors='surrogate_then_replace') 309 if rc == 0: 310 data['timestamping'] = [m.lower() for m in re.findall(r'SOF_TIMESTAMPING_(\w+)', stdout)] 311 data['hw_timestamp_filters'] = [m.lower() for m in re.findall(r'HWTSTAMP_FILTER_(\w+)', stdout)] 312 m = re.search(r'PTP Hardware Clock: (\d+)', stdout) 313 if m: 314 data['phc_index'] = int(m.groups()[0]) 315 316 return data 317 318 319class LinuxNetworkCollector(NetworkCollector): 320 _platform = 'Linux' 321 _fact_class = LinuxNetwork 322 required_facts = set(['distribution', 'platform']) 323