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