From 275c2c311f96efc3f64152f94a4d1089036ca343 Mon Sep 17 00:00:00 2001 From: Simon Maillard Date: Mon, 20 Nov 2023 14:45:23 +0100 Subject: [PATCH] Update NetworkDevices Fact * Add mtu, state, mac adress, to the output * Refactoring --- pyinfra/facts/hardware.py | 294 ++++++++++-------- .../hardware.Ipv4Addresses/linux_ip.json | 2 +- .../hardware.Ipv4Addrs/linux_ip_multiple.json | 2 +- .../hardware.Ipv6Addresses/linux_ip.json | 2 +- .../hardware.Ipv6Addrs/linux_ip_multiple.json | 2 +- .../linux_ifconfig.json | 91 ++++-- .../hardware.NetworkDevices/linux_ip.json | 10 +- .../linux_ip_multiple.json | 13 +- .../macos_ifconfig.json | 7 +- 9 files changed, 263 insertions(+), 160 deletions(-) diff --git a/pyinfra/facts/hardware.py b/pyinfra/facts/hardware.py index 81c598ee6..5f00eab9d 100644 --- a/pyinfra/facts/hardware.py +++ b/pyinfra/facts/hardware.py @@ -97,156 +97,196 @@ def process(self, output): return devices -nettools_1_regexes = [ - ( - r"^inet addr:([0-9\.]+).+Bcast:([0-9\.]+).+Mask:([0-9\.]+)$", - ("ipv4", "address", "broadcast", "netmask"), - ), - ( - r"^inet6 addr: ([0-9a-z:]+)\/([0-9]+) Scope:Global", - ("ipv6", "address", "mask_bits"), - ), -] - -nettools_2_regexes = [ - ( - r"^inet ([0-9\.]+)\s+netmask ([0-9\.fx]+)(?:\s+broadcast ([0-9\.]+))?$", - ("ipv4", "address", "netmask", "broadcast"), - ), - ( - r"^inet6 ([0-9a-z:]+)\s+prefixlen ([0-9]+)", - ("ipv6", "address", "mask_bits"), - ), -] - -iproute2_regexes = [ - ( - r"^inet ([0-9\.]+)\/([0-9]{1,2})(?:\s+brd ([0-9\.]+))?", - ("ipv4", "address", "mask_bits", "broadcast"), - ), - ( - r"^inet6 ([0-9a-z:]+)\/([0-9]{1,3})", - ("ipv6", "address", "mask_bits"), - ), -] - - -def _parse_regexes(regexes, lines): - data = { - "ipv4": {}, - "ipv6": {}, - } - - for line in lines: - for regex, groups in regexes: - matches = re.match(regex, line) - if matches: - ip_data = {} - - for i, group in enumerate(groups[1:]): - ip_data[group] = matches.group(i + 1) - - if "mask_bits" in ip_data: - ip_data["mask_bits"] = int(ip_data["mask_bits"]) - - target_group = data[groups[0]] - if target_group.get("address"): - target_group.setdefault("additional_ips", []).append(ip_data) - else: - target_group.update(ip_data) - - break - - return data - - class NetworkDevices(FactBase): """ Gets & returns a dict of network devices. See the ``ipv4_addresses`` and ``ipv6_addresses`` facts for easier-to-use shortcuts to get device addresses. .. code:: python - - { - "eth0": { - "ipv4": { - "address": "127.0.0.1", - "broadcast": "127.0.0.13", - # Only one of these will exist: - "netmask": "255.255.255.255", - "mask_bits": 32, - }, - "ipv6": { - "address": "fe80::a00:27ff:fec3:36f0", - "mask_bits": 64, - "additional_ips": [{ - "address": "fe80::", - "mask_bits": 128, - }], - } + "enp1s0": { + "ether": "12:34:56:78:9A:BC", + "mtu": 1500, + "state": "UP", + "ipv4": { + "address": "192.168.1.100", + "mask_bits": 24, + "netmask": "255.255.255.0" }, + "ipv6": { + "address": "2001:db8:85a3::8a2e:370:7334", + "mask_bits": 64, + "additional_ips": [ + { + "address": "fe80::1234:5678:9abc:def0", + "mask_bits": 64 + } + ] + } + }, + "incusbr0": { + "ether": "DE:AD:BE:EF:CA:FE", + "mtu": 1500, + "state": "UP", + "ipv4": { + "address": "10.0.0.1", + "mask_bits": 24, + "netmask": "255.255.255.0" + }, + "ipv6": { + "address": "fe80::dead:beef:cafe:babe", + "mask_bits": 64, + "additional_ips": [ + { + "address": "2001:db8:1234:5678::1", + "mask_bits": 64 + } + ] + } + }, + "lo": { + "mtu": 65536, + "state": "UP", + "ipv6": { + "address": "::1", + "mask_bits": 128 + } + }, + "veth98806fd6": { + "ether": "AA:BB:CC:DD:EE:FF", + "mtu": 1500, + "state": "UP" + }, + "vethda29df81": { + "ether": "11:22:33:44:55:66", + "mtu": 1500, + "state": "UP" + }, + "wlo1": { + "ether": "77:88:99:AA:BB:CC", + "mtu": 1500, + "state": "UNKNOWN" } """ - command = "ip addr show 2> /dev/null || ifconfig" + command = "ip addr show 2> /dev/null || ifconfig -a" default = dict # Definition of valid interface names for Linux: # https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/net/core/dev.c?h=v5.1.3#n1020 - _start_regexes = [ - ( - r"^([^/: \s]+)\s+Link encap:", - lambda lines: _parse_regexes(nettools_1_regexes, lines), - ), - ( - r"^([^/: \s]+): flags=", - lambda lines: _parse_regexes(nettools_2_regexes, lines), - ), - ( - r"^[0-9]+: ([^/: \s]+): ", - lambda lines: _parse_regexes(iproute2_regexes, lines), - ), - ] - def process(self, output): - devices = {} - - # Store current matches (start lines), the handler and any lines - matches = None - handler = None - line_buffer = [] + def mask(value): + try: + if value.startswith("0x"): + mask_bits = bin(int(value, 16)).count("1") + else: + mask_bits = int(value) + netmask = ".".join( + str((0xFFFFFFFF << (32 - b) >> mask_bits) & 0xFF) for b in (24, 16, 8, 0) + ) + except ValueError: + mask_bits = sum(bin(int(x)).count("1") for x in value.split(".")) + netmask = value - for line in output: - line = line.strip() + return mask_bits, netmask - matched = False + # Strip lines and merge them as a block of text + output = "\n".join(map(str.strip, output)) - # Look for start lines - for regex, new_handler in self._start_regexes: - new_matches = re.match(regex, line) + # Splitting the output into sections per network device + device_sections = re.split(r"\n(?=\d+: \w|\w+:.*mtu.*)", output) + print(device_sections, "\n") - # If we find a start line - if new_matches: - matched = True + # Dictionary to hold all device information + all_devices = {} - # Assign any current matches with current handler, reset buffer - if matches: - devices[matches.group(1)] = handler(line_buffer) - line_buffer = [] + for section in device_sections: + # Extracting the device name + device_name_match = re.match(r"^(?:\d+: )?([\w@]+):", section) + if not device_name_match: + continue + device_name = device_name_match.group(1) + + # Regular expressions to match different parts of the output + ether_re = re.compile(r"([0-9A-Fa-f:]{17})") + mtu_re = re.compile(r"mtu (\d+)") + ipv4_re = ( + re.compile( + r"inet (\d+\.\d+\.\d+\.\d+)/(\d+)(?: brd (\d+\.\d+\.\d+\.\d+))" + ), # ip a output, + re.compile( + r"inet (\d+\.\d+\.\d+\.\d+)\s+netmask\s+((?:\d+\.\d+\.\d+\.\d+)|(?:[0-9a-fA-FxX]+))(?:\s+broadcast\s+(\d+\.\d+\.\d+\.\d+))" # noqa: E501 + ), # ifconfig -a output + ) + + # Parsing the output + ether = ether_re.search(section) + mtu = mtu_re.search(section) + + # Building the result dictionary for the device + device_info = {} + if ether: + device_info["ether"] = ether.group(1) + if mtu: + device_info["mtu"] = int(mtu.group(1)) + + device_info["state"] = ( + "UP" if "UP" in section else "DOWN" if "DOWN" in section else "UNKNOWN" + ) + + # IPv4 Addresses + for ipv4_re_ in ipv4_re: + ipv4_matches = ipv4_re_.findall(section) + if ipv4_matches: + break - # Set new matches/handler - matches = new_matches - handler = new_handler + if ipv4_matches: + ipv4_info = [] + for ipv4 in ipv4_matches: + address = ipv4[0] + mask_value = ipv4[1] + mask_bits, netmask = mask(mask_value) + broadcast = ipv4[2] if len(ipv4) == 3 else None + + ipv4_info.append( + { + "address": address, + "mask_bits": mask_bits, + "netmask": netmask, + } + | {"broadcast": broadcast} + if broadcast + else {} + ) + device_info["ipv4"] = ipv4_info[0] + if len(ipv4_matches) > 1: + print(device_info["ipv4"]) + device_info["ipv4"]["additional_ips"] = ipv4_info[1:] + + print(device_info["ipv4"]) + # IPv6 Addresses + ipv6_re = ( + re.compile(r"inet6\s+([0-9a-fA-F:]+)/(\d+)"), + re.compile(r"inet6\s+([0-9a-fA-F:]+)\s+prefixlen\s+(\d+)"), + ) + + for ipv6_re_ in ipv6_re: + ipv6_matches = ipv6_re_.findall(section) + if ipv6_matches: break - if not matched: - line_buffer.append(line) + if ipv6_matches: + ipv6_info = [] + for ipv6 in ipv6_matches: + address = ipv6[0] + mask_bits = ipv6[1] or ipv6[2] + ipv6_info.append({"address": address, "mask_bits": int(mask_bits)}) + device_info["ipv6"] = ipv6_info[0] + if len(ipv6_matches) > 1: + device_info["ipv6"]["additional_ips"] = ipv6_info[1:] - # Handle any left over matches - if matches: - devices[matches.group(1)] = handler(line_buffer) + all_devices[device_name] = device_info - return devices + return all_devices class Ipv4Addrs(ShortFactBase): @@ -273,7 +313,7 @@ def process_data(self, data): ips = [] ip_details = details.get(self.ip_type) - if not ip_details: + if not ip_details or not ip_details.get("address"): continue ips.append(ip_details["address"]) @@ -332,7 +372,7 @@ def process_data(self, data): for interface, details in data.items(): ip_details = details.get(self.ip_type) - if not ip_details: + if not ip_details or not ip_details.get("address"): continue # pragma: no cover addresses[interface] = ip_details["address"] diff --git a/tests/facts/hardware.Ipv4Addresses/linux_ip.json b/tests/facts/hardware.Ipv4Addresses/linux_ip.json index 7bec41065..a6d24c413 100644 --- a/tests/facts/hardware.Ipv4Addresses/linux_ip.json +++ b/tests/facts/hardware.Ipv4Addresses/linux_ip.json @@ -1,5 +1,5 @@ { - "command": "ip addr show 2> /dev/null || ifconfig", + "command": "ip addr show 2> /dev/null || ifconfig -a", "output": [ "2: eth0: mtu 1500 qdisc fq_codel state UP group default qlen 1000", "link/ether 08:00:27:6d:95:c4 brd ff:ff:ff:ff:ff:ff", diff --git a/tests/facts/hardware.Ipv4Addrs/linux_ip_multiple.json b/tests/facts/hardware.Ipv4Addrs/linux_ip_multiple.json index c830e30fd..32f3583c6 100644 --- a/tests/facts/hardware.Ipv4Addrs/linux_ip_multiple.json +++ b/tests/facts/hardware.Ipv4Addrs/linux_ip_multiple.json @@ -1,5 +1,5 @@ { - "command": "ip addr show 2> /dev/null || ifconfig", + "command": "ip addr show 2> /dev/null || ifconfig -a", "output": [ "2: eth0: mtu 1500 qdisc fq_codel state UP group default qlen 1000", "link/ether c6:c5:60:cd:73:f1 brd ff:ff:ff:ff:ff:ff", diff --git a/tests/facts/hardware.Ipv6Addresses/linux_ip.json b/tests/facts/hardware.Ipv6Addresses/linux_ip.json index cff0191be..6d75ed496 100644 --- a/tests/facts/hardware.Ipv6Addresses/linux_ip.json +++ b/tests/facts/hardware.Ipv6Addresses/linux_ip.json @@ -1,5 +1,5 @@ { - "command": "ip addr show 2> /dev/null || ifconfig", + "command": "ip addr show 2> /dev/null || ifconfig -a", "output": [ "2: eth0: mtu 1500 qdisc fq_codel state UP group default qlen 1000", "link/ether 08:00:27:6d:95:c4 brd ff:ff:ff:ff:ff:ff", diff --git a/tests/facts/hardware.Ipv6Addrs/linux_ip_multiple.json b/tests/facts/hardware.Ipv6Addrs/linux_ip_multiple.json index 8751c79c6..55e31908d 100644 --- a/tests/facts/hardware.Ipv6Addrs/linux_ip_multiple.json +++ b/tests/facts/hardware.Ipv6Addrs/linux_ip_multiple.json @@ -1,5 +1,5 @@ { - "command": "ip addr show 2> /dev/null || ifconfig", + "command": "ip addr show 2> /dev/null || ifconfig -a", "output": [ "2: eth0: mtu 1500 qdisc fq_codel state UP group default qlen 1000", "link/ether c6:c5:60:cd:73:f1 brd ff:ff:ff:ff:ff:ff", diff --git a/tests/facts/hardware.NetworkDevices/linux_ifconfig.json b/tests/facts/hardware.NetworkDevices/linux_ifconfig.json index 83c426887..4638b614d 100644 --- a/tests/facts/hardware.NetworkDevices/linux_ifconfig.json +++ b/tests/facts/hardware.NetworkDevices/linux_ifconfig.json @@ -1,32 +1,83 @@ { - "command": "ip addr show 2> /dev/null || ifconfig", + "command": "ip addr show 2> /dev/null || ifconfig -a", "output": [ - "eth0 Link encap:Ethernet HWaddr 14:DD:A9:D3:36:0B", - "inet addr:1.2.3.4 Bcast:1.2.3.4 Mask:255.255.255.255", - "inet6 addr: 2a01::01/64 Scope:Global", - "UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1", - "RX packets:5088726642662 errors:0 dropped:0 overruns:75824296 frame:0", - "TX packets:204488770319 errors:0 dropped:0 overruns:0 carrier:0", - "collisions:0 txqueuelen:1000", - "RX bytes:1110992903295696 (1010.4 TiB) TX bytes:68385443441652 (62.1 TiB)", - "Memory:fa100000-fa17ffff", - "eth1 Link encap:Ethernet HWaddr 14:DD:A9:D3:36:0B" + "enp1s0: flags=4163 mtu 1500", + " inet 192.168.123.111 netmask 255.255.255.0 broadcast 192.168.123.255", + " inet6 2a01:e0a:5c2:7450:b241:6fff:fe0a:cf22 prefixlen 64 scopeid 0x0", + " inet6 fe80::b241:6fff:fe0a:cf22 prefixlen 64 scopeid 0x20", + " ether b0:41:6f:0a:cf:22 txqueuelen 1000 (Ethernet)", + " RX packets 52490 bytes 16827945 (16.0 MiB)", + " RX errors 0 dropped 0 overruns 0 frame 0", + " TX packets 21304 bytes 2948965 (2.8 MiB)", + " TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0", + "incusbr0: flags=4163 mtu 1500", + " inet 10.232.181.1 netmask 255.255.255.0 broadcast 0.0.0.0", + " inet6 fe80::216:3eff:fe9c:8200 prefixlen 64 scopeid 0x20", + " inet6 fd42:5bce:bd73:eda6::1 prefixlen 64 scopeid 0x0", + " ether 00:16:3e:9c:82:00 txqueuelen 1000 (Ethernet)", + " RX packets 2592 bytes 253292 (247.3 KiB)", + " RX errors 0 dropped 0 overruns 0 frame 0", + " TX packets 1289 bytes 185014 (180.6 KiB)", + " TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0", + "lo: flags=73 mtu 65536", + " inet 127.0.0.1 netmask 255.0.0.0", + " inet6 ::1 prefixlen 128 scopeid 0x10", + " loop txqueuelen 1000 (Local Loopback)", + " RX packets 34 bytes 2816 (2.7 KiB)", + " RX errors 0 dropped 0 overruns 0 frame 0", + " TX packets 34 bytes 2816 (2.7 KiB)", + " TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0" ], "fact": { - "eth0": { + "enp1s0": { + "ether": "2a01:e0a:5c2:7450", + "mtu": 1500, + "state": "UP", "ipv4": { - "address": "1.2.3.4", - "broadcast": "1.2.3.4", - "netmask": "255.255.255.255" + "address": "192.168.123.111", + "mask_bits": 24, + "netmask": "255.255.255.0", + "broadcast": "192.168.123.255" }, "ipv6": { - "address": "2a01::01", - "mask_bits": 64 + "address": "2a01:e0a:5c2:7450:b241:6fff:fe0a:cf22", + "mask_bits": 64, + "additional_ips": [ + { + "address": "fe80::b241:6fff:fe0a:cf22", + "mask_bits": 64 + } + ] } }, - "eth1": { - "ipv4": {}, - "ipv6": {} + "incusbr0": { + "ether": "fe80::216:3eff:fe", + "mtu": 1500, + "state": "UP", + "ipv4": { + "address": "10.232.181.1", + "mask_bits": 24, + "netmask": "255.255.255.0", + "broadcast": "0.0.0.0" + }, + "ipv6": { + "address": "fe80::216:3eff:fe9c:8200", + "mask_bits": 64, + "additional_ips": [ + { + "address": "fd42:5bce:bd73:eda6::1", + "mask_bits": 64 + } + ] + } + }, + "lo": { + "mtu": 65536, + "state": "UP", + "ipv6": { + "address": "::1", + "mask_bits": 128 + } } } } diff --git a/tests/facts/hardware.NetworkDevices/linux_ip.json b/tests/facts/hardware.NetworkDevices/linux_ip.json index 879670fa6..90d0cf69a 100644 --- a/tests/facts/hardware.NetworkDevices/linux_ip.json +++ b/tests/facts/hardware.NetworkDevices/linux_ip.json @@ -1,5 +1,5 @@ { - "command": "ip addr show 2> /dev/null || ifconfig", + "command": "ip addr show 2> /dev/null || ifconfig -a", "output": [ "2: eth0: mtu 1500 qdisc fq_codel state UP group default qlen 1000", "link/ether 08:00:27:6d:95:c4 brd ff:ff:ff:ff:ff:ff", @@ -10,10 +10,14 @@ ], "fact": { "eth0": { + "ether": "08:00:27:6d:95:c4", + "mtu": 1500, + "state": "UP", "ipv4": { "address": "1.2.3.4", - "broadcast": "1.2.3.4", - "mask_bits": 24 + "mask_bits": 24, + "netmask": "255.255.255.0", + "broadcast": "1.2.3.4" }, "ipv6": { "address": "2a01::01", diff --git a/tests/facts/hardware.NetworkDevices/linux_ip_multiple.json b/tests/facts/hardware.NetworkDevices/linux_ip_multiple.json index cb13d872c..e66578f01 100644 --- a/tests/facts/hardware.NetworkDevices/linux_ip_multiple.json +++ b/tests/facts/hardware.NetworkDevices/linux_ip_multiple.json @@ -1,5 +1,5 @@ { - "command": "ip addr show 2> /dev/null || ifconfig", + "command": "ip addr show 2> /dev/null || ifconfig -a", "output": [ "2: eth0: mtu 1500 qdisc fq_codel state UP group default qlen 1000", "link/ether c6:c5:60:cd:73:f1 brd ff:ff:ff:ff:ff:ff", @@ -12,14 +12,19 @@ ], "fact": { "eth0": { + "ether": "c6:c5:60:cd:73:f1", + "mtu": 1500, + "state": "UP", "ipv4": { "address": "166.22.203.666", - "broadcast": "165.22.207.255", "mask_bits": 20, + "netmask": "255.255.240.0", + "broadcast": "165.22.207.255", "additional_ips": [{ "address": "10.18.0.10", - "broadcast": "10.18.255.255", - "mask_bits": 16 + "mask_bits": 16, + "netmask": "255.255.0.0", + "broadcast": "10.18.255.255" }] }, "ipv6": { diff --git a/tests/facts/hardware.NetworkDevices/macos_ifconfig.json b/tests/facts/hardware.NetworkDevices/macos_ifconfig.json index d1dd82336..4b971fee5 100644 --- a/tests/facts/hardware.NetworkDevices/macos_ifconfig.json +++ b/tests/facts/hardware.NetworkDevices/macos_ifconfig.json @@ -1,5 +1,5 @@ { - "command": "ip addr show 2> /dev/null || ifconfig", + "command": "ip addr show 2> /dev/null || ifconfig -a", "output": [ "en0: flags=8863 mtu 1500", "inet 1.2.3.4 netmask 0xffffff00 broadcast 255.255.255.255", @@ -10,9 +10,12 @@ ], "fact": { "en0": { + "mtu": 1500, + "state": "UP", "ipv4": { "address": "1.2.3.4", - "netmask": "0xffffff00", + "mask_bits": 24, + "netmask": "255.255.255.0", "broadcast": "255.255.255.255" }, "ipv6": {