diff --git a/pyinfra/facts/hardware.py b/pyinfra/facts/hardware.py
index f8c36794f..81f394d75 100644
--- a/pyinfra/facts/hardware.py
+++ b/pyinfra/facts/hardware.py
@@ -195,28 +195,36 @@ def mask(value):
output = "\n".join(map(str.strip, output))
# Splitting the output into sections per network device
- device_sections = re.split(r"\n(?=\d+: \w|\w+:.*mtu.*)", output)
+ device_sections = re.split(r"\n(?=\d+: [^\s/:]|[^\s/:]+:.*mtu )", output)
# Dictionary to hold all device information
all_devices = {}
for section in device_sections:
# Extracting the device name
- device_name_match = re.match(r"^(?:\d+: )?([\w@]+):", section)
+ device_name_match = re.match(r"^(?:\d+: )?([^\s/:]+):", 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})")
+ ether_re = re.compile(r"ether ([0-9A-Fa-f:]{17})")
mtu_re = re.compile(r"mtu (\d+)")
ipv4_re = (
+ # ip a
re.compile(
- r"inet (\d+\.\d+\.\d+\.\d+)/(\d+)(?: brd (\d+\.\d+\.\d+\.\d+))"
- ), # ip a output,
+ r"inet (?P
\d+\.\d+\.\d+\.\d+)/(?P\d+)(?: metric \d+)?(?: brd (?P\d+\.\d+\.\d+\.\d+))?" # noqa: E501
+ ),
+ # ifconfig -a
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
+ r"inet (?P\d+\.\d+\.\d+\.\d+)\s+netmask\s+(?P(?:\d+\.\d+\.\d+\.\d+)|(?:[0-9a-fA-FxX]+))(?:\s+broadcast\s+(?P\d+\.\d+\.\d+\.\d+))?" # noqa: E501
+ ),
+ )
+ ipv6_re = (
+ # ip a
+ re.compile(r"inet6\s+(?P[0-9a-fA-F:]+)/(?P\d+)"),
+ # ifconfig -a
+ re.compile(r"inet6\s+(?P[0-9a-fA-F:]+)\s+prefixlen\s+(?P\d+)"),
)
# Parsing the output
@@ -235,18 +243,22 @@ def mask(value):
)
# IPv4 Addresses
+ ipv4_matches: list[re.Match[str]]
for ipv4_re_ in ipv4_re:
- ipv4_matches = ipv4_re_.findall(section)
- if ipv4_matches:
+ ipv4_matches = list(ipv4_re_.finditer(section))
+ if len(ipv4_matches):
break
- if ipv4_matches:
+ if len(ipv4_matches):
ipv4_info = []
for ipv4 in ipv4_matches:
- address = ipv4[0]
- mask_value = ipv4[1]
+ address = ipv4.group("address")
+ mask_value = ipv4.group("mask")
mask_bits, netmask = mask(mask_value)
- broadcast = ipv4[2] if len(ipv4) == 3 else None
+ try:
+ broadcast = ipv4.group("broadcast")
+ except IndexError:
+ broadcast = None
ipv4_info.append(
{
@@ -261,21 +273,17 @@ def mask(value):
device_info["ipv4"]["additional_ips"] = ipv4_info[1:] # type: ignore[index]
# 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+)"),
- )
-
+ ipv6_matches: list[re.Match[str]]
for ipv6_re_ in ipv6_re:
- ipv6_matches = ipv6_re_.findall(section)
+ ipv6_matches = list(ipv6_re_.finditer(section))
if ipv6_matches:
break
- if ipv6_matches:
+ if len(ipv6_matches):
ipv6_info = []
for ipv6 in ipv6_matches:
- address = ipv6[0]
- mask_bits = ipv6[1] or ipv6[2]
+ address = ipv6.group("address")
+ mask_bits = ipv6.group("mask")
ipv6_info.append({"address": address, "mask_bits": int(mask_bits)})
device_info["ipv6"] = ipv6_info[0]
if len(ipv6_matches) > 1:
diff --git a/tests/facts/hardware.NetworkDevices/linux_ifconfig.json b/tests/facts/hardware.NetworkDevices/linux_ifconfig.json
index 4638b614d..169d619bd 100644
--- a/tests/facts/hardware.NetworkDevices/linux_ifconfig.json
+++ b/tests/facts/hardware.NetworkDevices/linux_ifconfig.json
@@ -30,7 +30,7 @@
],
"fact": {
"enp1s0": {
- "ether": "2a01:e0a:5c2:7450",
+ "ether": "b0:41:6f:0a:cf:22",
"mtu": 1500,
"state": "UP",
"ipv4": {
@@ -51,7 +51,7 @@
}
},
"incusbr0": {
- "ether": "fe80::216:3eff:fe",
+ "ether": "00:16:3e:9c:82:00",
"mtu": 1500,
"state": "UP",
"ipv4": {
@@ -74,6 +74,12 @@
"lo": {
"mtu": 65536,
"state": "UP",
+ "ipv4": {
+ "address": "127.0.0.1",
+ "mask_bits": 8,
+ "netmask": "255.0.0.0",
+ "broadcast": null
+ },
"ipv6": {
"address": "::1",
"mask_bits": 128
diff --git a/tests/facts/hardware.NetworkDevices/linux_ifconfig_multiple.json b/tests/facts/hardware.NetworkDevices/linux_ifconfig_multiple.json
new file mode 100644
index 000000000..acb2ee471
--- /dev/null
+++ b/tests/facts/hardware.NetworkDevices/linux_ifconfig_multiple.json
@@ -0,0 +1,197 @@
+{
+ "command": "ip addr show 2> /dev/null || ifconfig -a",
+ "output": [
+ "myvlan: flags=4163 mtu 1500",
+ " inet 10.42.13.147 netmask 255.255.255.0 broadcast 10.42.13.255",
+ " inet6 fe80::3cd2:56ff:fe4d:1af2 prefixlen 64 scopeid 0x20",
+ " ether 3e:d2:56:4d:1a:f2 txqueuelen 1000 (Ethernet)",
+ " RX packets 115016 bytes 28450084 (27.1 MiB)",
+ " RX errors 0 dropped 0 overruns 0 frame 0",
+ " TX packets 53310 bytes 9803632 (9.3 MiB)",
+ " TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0",
+ "",
+ "eno1: flags=4163 mtu 1500",
+ " inet 10.42.6.142 netmask 255.255.255.0 broadcast 10.42.6.255",
+ " inet6 fe80::3cd2:56ff:fe4d:1af1 prefixlen 64 scopeid 0x20",
+ " ether 3e:d2:56:4d:1a:f1 txqueuelen 1000 (Ethernet)",
+ " RX packets 5097072 bytes 4315873036 (4.0 GiB)",
+ " RX errors 0 dropped 0 overruns 0 frame 0",
+ " TX packets 3897172 bytes 1574206075 (1.4 GiB)",
+ " 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 9470290 bytes 4905913998 (4.5 GiB)",
+ " RX errors 0 dropped 0 overruns 0 frame 0",
+ " TX packets 9470290 bytes 4905913998 (4.5 GiB)",
+ " TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0",
+ "",
+ "my-vpn: flags=4305 mtu 1300",
+ " inet 10.66.7.3 netmask 255.255.255.0 destination 10.66.7.3",
+ " inet6 fe80::c9fc:e8d5:12d8:cfb2 prefixlen 64 scopeid 0x20",
+ " unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 500 (UNSPEC)",
+ " RX packets 59069 bytes 22687707 (21.6 MiB)",
+ " RX errors 0 dropped 0 overruns 0 frame 0",
+ " TX packets 55603 bytes 8340765 (7.9 MiB)",
+ " TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0",
+ "",
+ "some-br: flags=4163 mtu 1500",
+ " inet 192.168.101.1 netmask 255.255.255.0 broadcast 0.0.0.0",
+ " inet6 fe80::f884:69ff:fe0a:a073 prefixlen 64 scopeid 0x20",
+ " ether 8e:15:02:8a:7f:ee txqueuelen 1000 (Ethernet)",
+ " RX packets 1315769 bytes 616685980 (588.1 MiB)",
+ " RX errors 0 dropped 0 overruns 0 frame 0",
+ " TX packets 1434638 bytes 628356665 (599.2 MiB)",
+ " TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0",
+ "",
+ "some-tap: flags=4163 mtu 1500",
+ " inet6 fe80::f884:69ff:fe0a:a073 prefixlen 64 scopeid 0x20",
+ " ether fa:84:69:0a:a0:73 txqueuelen 1000 (Ethernet)",
+ " RX packets 1315769 bytes 635106746 (605.6 MiB)",
+ " RX errors 0 dropped 0 overruns 0 frame 0",
+ " TX packets 1450904 bytes 631656508 (602.3 MiB)",
+ " TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0",
+ "",
+ "vmbr0: flags=4099 mtu 1500",
+ " inet 172.20.0.2 netmask 255.255.255.0 broadcast 172.20.0.255",
+ " ether 7a:c8:eb:21:98:98 txqueuelen 1000 (Ethernet)",
+ " RX packets 0 bytes 0 (0.0 B)",
+ " RX errors 0 dropped 0 overruns 0 frame 0",
+ " TX packets 0 bytes 0 (0.0 B)",
+ " TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0",
+ "",
+ "vmbr0-veth: flags=4099 mtu 1500",
+ " ether 52:a5:03:cb:ee:31 txqueuelen 1000 (Ethernet)",
+ " RX packets 0 bytes 0 (0.0 B)",
+ " RX errors 0 dropped 0 overruns 0 frame 0",
+ " TX packets 0 bytes 0 (0.0 B)",
+ " TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0",
+ "",
+ "vmbr0-veth-end: flags=4098 mtu 1500",
+ " ether 96:1d:5c:3b:20:9e txqueuelen 1000 (Ethernet)",
+ " RX packets 0 bytes 0 (0.0 B)",
+ " RX errors 0 dropped 0 overruns 0 frame 0",
+ " TX packets 0 bytes 0 (0.0 B)",
+ " TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0",
+ "",
+ "wlp6s0: flags=4098 mtu 1500",
+ " ether b8:4c:68:59:45:7d txqueuelen 1000 (Ethernet)",
+ " RX packets 0 bytes 0 (0.0 B)",
+ " RX errors 0 dropped 0 overruns 0 frame 0",
+ " TX packets 0 bytes 0 (0.0 B)",
+ " TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0",
+ ""
+ ],
+ "fact": {
+ "lo": {
+ "mtu": 65536,
+ "state": "UP",
+ "ipv4": {
+ "address": "127.0.0.1",
+ "mask_bits": 8,
+ "netmask": "255.0.0.0",
+ "broadcast": null
+ },
+ "ipv6": {
+ "address": "::1",
+ "mask_bits": 128
+ }
+ },
+ "eno1": {
+ "ether": "3e:d2:56:4d:1a:f1",
+ "mtu": 1500,
+ "state": "UP",
+ "ipv4": {
+ "address": "10.42.6.142",
+ "mask_bits": 24,
+ "netmask": "255.255.255.0",
+ "broadcast": "10.42.6.255"
+ },
+ "ipv6": {
+ "address": "fe80::3cd2:56ff:fe4d:1af1",
+ "mask_bits": 64
+ }
+ },
+ "wlp6s0": {
+ "ether": "b8:4c:68:59:45:7d",
+ "mtu": 1500,
+ "state": "UNKNOWN"
+ },
+ "myvlan": {
+ "ether": "3e:d2:56:4d:1a:f2",
+ "mtu": 1500,
+ "state": "UP",
+ "ipv4": {
+ "address": "10.42.13.147",
+ "mask_bits": 24,
+ "netmask": "255.255.255.0",
+ "broadcast": "10.42.13.255"
+ },
+ "ipv6": {
+ "address": "fe80::3cd2:56ff:fe4d:1af2",
+ "mask_bits": 64
+ }
+ },
+ "vmbr0": {
+ "ether": "7a:c8:eb:21:98:98",
+ "mtu": 1500,
+ "state": "UP",
+ "ipv4": {
+ "address": "172.20.0.2",
+ "mask_bits": 24,
+ "netmask": "255.255.255.0",
+ "broadcast": "172.20.0.255"
+ }
+ },
+ "vmbr0-veth-end": {
+ "ether": "96:1d:5c:3b:20:9e",
+ "mtu": 1500,
+ "state": "UNKNOWN"
+ },
+ "vmbr0-veth": {
+ "ether": "52:a5:03:cb:ee:31",
+ "mtu": 1500,
+ "state": "UP"
+ },
+ "some-tap": {
+ "ether": "fa:84:69:0a:a0:73",
+ "mtu": 1500,
+ "state": "UP",
+ "ipv6": {
+ "address": "fe80::f884:69ff:fe0a:a073",
+ "mask_bits": 64
+ }
+ },
+ "some-br": {
+ "ether": "8e:15:02:8a:7f:ee",
+ "mtu": 1500,
+ "state": "UP",
+ "ipv4": {
+ "address": "192.168.101.1",
+ "mask_bits": 24,
+ "netmask": "255.255.255.0",
+ "broadcast": "0.0.0.0"
+ },
+ "ipv6": {
+ "address": "fe80::f884:69ff:fe0a:a073",
+ "mask_bits": 64
+ }
+ },
+ "my-vpn": {
+ "mtu": 1300,
+ "state": "UP",
+ "ipv4": {
+ "address": "10.66.7.3",
+ "mask_bits": 24,
+ "netmask": "255.255.255.0",
+ "broadcast": null
+ },
+ "ipv6": {
+ "address": "fe80::c9fc:e8d5:12d8:cfb2",
+ "mask_bits": 64
+ }
+ }
+ }
+}
diff --git a/tests/facts/hardware.NetworkDevices/linux_ip_multiple.json b/tests/facts/hardware.NetworkDevices/linux_ip_multiple_addrs.json
similarity index 100%
rename from tests/facts/hardware.NetworkDevices/linux_ip_multiple.json
rename to tests/facts/hardware.NetworkDevices/linux_ip_multiple_addrs.json
diff --git a/tests/facts/hardware.NetworkDevices/linux_ip_multiple_ifaces.json b/tests/facts/hardware.NetworkDevices/linux_ip_multiple_ifaces.json
new file mode 100644
index 000000000..af5d277a1
--- /dev/null
+++ b/tests/facts/hardware.NetworkDevices/linux_ip_multiple_ifaces.json
@@ -0,0 +1,160 @@
+{
+ "command": "ip addr show 2> /dev/null || ifconfig -a",
+ "output": [
+ "1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000",
+ " link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00",
+ " inet 127.0.0.1/8 scope host lo",
+ " valid_lft forever preferred_lft forever",
+ " inet6 ::1/128 scope host noprefixroute ",
+ " valid_lft forever preferred_lft forever",
+ "2: eno1: mtu 1500 qdisc fq_codel state UP group default qlen 1000",
+ " link/ether 3e:d2:56:4d:1a:f1 brd ff:ff:ff:ff:ff:ff",
+ " altname enp4s0",
+ " inet 10.42.6.142/24 brd 10.42.6.255 scope global dynamic noprefixroute eno1",
+ " valid_lft 53778sec preferred_lft 53778sec",
+ " inet6 fe80::3cd2:56ff:fe4d:1af1/64 scope link proto kernel_ll ",
+ " valid_lft forever preferred_lft forever",
+ "3: wlp6s0: mtu 1500 qdisc noop state DOWN group default qlen 1000",
+ " link/ether b8:4c:68:59:45:7d brd ff:ff:ff:ff:ff:ff permaddr 64:5d:24:b6:ad:5c",
+ "4: myvlan@eno1: mtu 1500 qdisc noqueue state UP group default qlen 1000",
+ " link/ether 3e:d2:56:4d:1a:f2 brd ff:ff:ff:ff:ff:ff",
+ " inet 10.42.13.147/24 brd 10.42.13.255 scope global dynamic noprefixroute myvlan",
+ " valid_lft 58557sec preferred_lft 58557sec",
+ " inet6 fe80::3cd2:56ff:fe4d:1af2/64 scope link proto kernel_ll ",
+ " valid_lft forever preferred_lft forever",
+ "5: vmbr0: mtu 1500 qdisc noqueue state DOWN group default qlen 1000",
+ " link/ether 7a:c8:eb:21:98:98 brd ff:ff:ff:ff:ff:ff",
+ " inet 172.20.0.2/24 brd 172.20.0.255 scope global noprefixroute vmbr0",
+ " valid_lft forever preferred_lft forever",
+ "6: vmbr0-veth-end@vmbr0-veth: mtu 1500 qdisc noop state DOWN group default qlen 1000",
+ " link/ether 96:1d:5c:3b:20:9e brd ff:ff:ff:ff:ff:ff",
+ "7: vmbr0-veth@vmbr0-veth-end: mtu 1500 qdisc noqueue state LOWERLAYERDOWN group default qlen 1000",
+ " link/ether 52:a5:03:cb:ee:31 brd ff:ff:ff:ff:ff:ff",
+ "8: some-tap: mtu 1500 qdisc fq_codel master some-br state UNKNOWN group default qlen 1000",
+ " link/ether fa:84:69:0a:a0:73 brd ff:ff:ff:ff:ff:ff",
+ " inet6 fe80::f884:69ff:fe0a:a073/64 scope link proto kernel_ll ",
+ " valid_lft forever preferred_lft forever",
+ "9: some-br: mtu 1500 qdisc noqueue state UP group default qlen 1000",
+ " link/ether 8e:15:02:8a:7f:ee brd ff:ff:ff:ff:ff:ff",
+ " inet 192.168.101.1/24 scope global some-br",
+ " valid_lft forever preferred_lft forever",
+ " inet6 fe80::f884:69ff:fe0a:a073/64 scope link proto kernel_ll ",
+ " valid_lft forever preferred_lft forever",
+ "14: my-vpn: mtu 1300 qdisc fq_codel state UNKNOWN group default qlen 500",
+ " link/none ",
+ " inet 10.66.7.3/24 scope global my-vpn",
+ " valid_lft forever preferred_lft forever",
+ " inet6 fe80::c9fc:e8d5:12d8:cfb2/64 scope link stable-privacy proto kernel_ll ",
+ " valid_lft forever preferred_lft forever"
+ ],
+ "fact": {
+ "lo": {
+ "mtu": 65536,
+ "state": "UP",
+ "ipv4": {
+ "address": "127.0.0.1",
+ "mask_bits": 8,
+ "netmask": "255.0.0.0",
+ "broadcast": null
+ },
+ "ipv6": {
+ "address": "::1",
+ "mask_bits": 128
+ }
+ },
+ "eno1": {
+ "ether": "3e:d2:56:4d:1a:f1",
+ "mtu": 1500,
+ "state": "UP",
+ "ipv4": {
+ "address": "10.42.6.142",
+ "mask_bits": 24,
+ "netmask": "255.255.255.0",
+ "broadcast": "10.42.6.255"
+ },
+ "ipv6": {
+ "address": "fe80::3cd2:56ff:fe4d:1af1",
+ "mask_bits": 64
+ }
+ },
+ "wlp6s0": {
+ "ether": "b8:4c:68:59:45:7d",
+ "mtu": 1500,
+ "state": "DOWN"
+ },
+ "myvlan@eno1": {
+ "ether": "3e:d2:56:4d:1a:f2",
+ "mtu": 1500,
+ "state": "UP",
+ "ipv4": {
+ "address": "10.42.13.147",
+ "mask_bits": 24,
+ "netmask": "255.255.255.0",
+ "broadcast": "10.42.13.255"
+ },
+ "ipv6": {
+ "address": "fe80::3cd2:56ff:fe4d:1af2",
+ "mask_bits": 64
+ }
+ },
+ "vmbr0": {
+ "ether": "7a:c8:eb:21:98:98",
+ "mtu": 1500,
+ "state": "UP",
+ "ipv4": {
+ "address": "172.20.0.2",
+ "mask_bits": 24,
+ "netmask": "255.255.255.0",
+ "broadcast": "172.20.0.255"
+ }
+ },
+ "vmbr0-veth-end@vmbr0-veth": {
+ "ether": "96:1d:5c:3b:20:9e",
+ "mtu": 1500,
+ "state": "DOWN"
+ },
+ "vmbr0-veth@vmbr0-veth-end": {
+ "ether": "52:a5:03:cb:ee:31",
+ "mtu": 1500,
+ "state": "UP"
+ },
+ "some-tap": {
+ "ether": "fa:84:69:0a:a0:73",
+ "mtu": 1500,
+ "state": "UP",
+ "ipv6": {
+ "address": "fe80::f884:69ff:fe0a:a073",
+ "mask_bits": 64
+ }
+ },
+ "some-br": {
+ "ether": "8e:15:02:8a:7f:ee",
+ "mtu": 1500,
+ "state": "UP",
+ "ipv4": {
+ "address": "192.168.101.1",
+ "mask_bits": 24,
+ "netmask": "255.255.255.0",
+ "broadcast": null
+ },
+ "ipv6": {
+ "address": "fe80::f884:69ff:fe0a:a073",
+ "mask_bits": 64
+ }
+ },
+ "my-vpn": {
+ "mtu": 1300,
+ "state": "UP",
+ "ipv4": {
+ "address": "10.66.7.3",
+ "mask_bits": 24,
+ "netmask": "255.255.255.0",
+ "broadcast": null
+ },
+ "ipv6": {
+ "address": "fe80::c9fc:e8d5:12d8:cfb2",
+ "mask_bits": 64
+ }
+ }
+ }
+}