Files
vacuum-wall/tests/test_network_integration.py
T
mteehan 318d7169f7 Switch networkctl parsing from text to JSON output
Replace fragile text-based parsing of ● 1: lo
                   Link File: n/a
                Network File: n/a
                       State: carrier (unmanaged)
                Online state: unknown
                        Type: loopback
            Hardware Address: 00:00:00:00:00:00
                         MTU: 65536
                       QDisc: noqueue
IPv6 Address Generation Mode: eui64
    Number of Queues (Tx/Rx): 1/1
                     Address: 127.0.0.1
                              ::1

May 30 22:43:27 vacuum-wall systemd-networkd[315]: lo: Link UP
May 30 22:43:27 vacuum-wall systemd-networkd[315]: lo: Gained carrier

● 77: eth0
                   Link File: /usr/lib/systemd/network/99-default.link
                Network File: /etc/systemd/network/eth0.network
                       State: routable (configured)
                Online state: online
                        Type: ether
                        Kind: veth
                      Driver: veth
            Hardware Address: 8e:63:52:6b:ea:e8
                         MTU: 1500 (min: 68, max: 65535)
                       QDisc: noqueue
IPv6 Address Generation Mode: eui64
    Number of Queues (Tx/Rx): 8/8
            Auto negotiation: no
                       Speed: 10Gbps
                      Duplex: full
                        Port: tp
                     Address: 192.168.1.5 (DHCPv4 via 192.168.1.1)
                              2600:4040:a6c1:4a00:8c63:52ff:fe6b:eae8
                              fe80::8c63:52ff:fe6b:eae8
                     Gateway: 192.168.1.1
                              fe80::3ebd:c5ff:fe2b:bd99
                         DNS: 192.168.1.1
                              2600:4040:a6c1:4a00::1
              Search Domains: myfiosgateway.com
           Activation Policy: up
         Required For Online: yes
            DHCPv4 Client ID: 8e:63:52:6b:ea:e8
          DHCPv6 Client IAID: 0xf3d61521
          DHCPv6 Client DUID: DUID-EN/Vendor:0000ab11b94215a519e8ca54

May 30 22:43:27 vacuum-wall systemd-networkd[315]: eth0: Link UP
May 30 22:43:27 vacuum-wall systemd-networkd[315]: eth0: Gained carrier
May 30 22:43:27 vacuum-wall systemd-networkd[315]: eth0: Configuring with /etc/systemd/network/eth0.network.
May 30 22:43:27 vacuum-wall systemd-networkd[315]: eth0: Gained IPv6LL
May 30 22:43:27 vacuum-wall systemd-networkd[315]: eth0: DHCPv4 address 192.168.1.5/24, gateway 192.168.1.1 acquired from 192.168.1.1

● 79: eth1
                   Link File: /usr/lib/systemd/network/99-default.link
                Network File: /etc/systemd/network/50-eth1.network
                       State: routable (configured)
                Online state: online
                        Type: ether
                        Kind: veth
                      Driver: veth
            Hardware Address: 8e:63:52:6b:ea:8e
                         MTU: 1500 (min: 68, max: 65535)
                       QDisc: noqueue
IPv6 Address Generation Mode: eui64
    Number of Queues (Tx/Rx): 8/8
            Auto negotiation: no
                       Speed: 10Gbps
                      Duplex: full
                        Port: tp
                     Address: 10.4.20.1
                              fd42:a304:c836:2a7f:8c63:52ff:fe6b:ea8e
                              fe80::8c63:52ff:fe6b:ea8e
                     Gateway: fe80::1266:6aff:fe76:bc5b
                         DNS: fd42:a304:c836:2a7f::1
           Activation Policy: up
         Required For Online: yes
          DHCPv6 Client IAID: 0x1da7c7a5
          DHCPv6 Client DUID: DUID-EN/Vendor:0000ab11b94215a519e8ca54

Jun 01 03:48:43 vacuum-wall systemd-networkd[315]: eth1: Reconfiguring with /etc/systemd/network/50-eth1.network.
Jun 01 04:05:47 vacuum-wall systemd-networkd[315]: eth1: Reconfiguring with /etc/systemd/network/99-eth1.network.
Jun 01 04:05:47 vacuum-wall systemd-networkd[315]: eth1: DHCPv6 lease lost
Jun 01 04:05:47 vacuum-wall systemd-networkd[315]: eth1: Reconfiguring with /etc/systemd/network/99-eth1.network.
Jun 01 04:06:51 vacuum-wall systemd-networkd[315]: eth1: Reconfiguring with /etc/systemd/network/99-eth1.network.
Jun 01 04:06:51 vacuum-wall systemd-networkd[315]: eth1: DHCPv6 lease lost
Jun 01 04:06:51 vacuum-wall systemd-networkd[315]: eth1: Reconfiguring with /etc/systemd/network/99-eth1.network.
Jun 01 04:09:35 vacuum-wall systemd-networkd[315]: eth1: Reconfiguring with /etc/systemd/network/50-eth1.network.
Jun 01 04:09:35 vacuum-wall systemd-networkd[315]: eth1: DHCPv6 lease lost
Jun 01 04:09:35 vacuum-wall systemd-networkd[315]: eth1: Reconfiguring with /etc/systemd/network/50-eth1.network. with structured JSON parsing using {"Interfaces":[{"Index":1,"Name":"lo","Type":"loopback","Flags":65609,"FlagsString":"up,loopback,running,lower-up","KernelOperationalState":0,"KernelOperationalStateString":"unknown","MTU":65536,"MinimumMTU":0,"MaximumMTU":4294967295,"AdministrativeState":"unmanaged","OperationalState":"carrier","CarrierState":"carrier","AddressState":"off","IPv4AddressState":"off","IPv6AddressState":"off","OnlineState":null,"Addresses":[{"Family":2,"Address":[127,0,0,1],"PrefixLength":8,"ConfigSource":"foreign","Scope":254,"ScopeString":"host","Flags":128,"FlagsString":"permanent","ConfigState":"configured"},{"Family":10,"Address":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],"PrefixLength":128,"ConfigSource":"foreign","Scope":254,"ScopeString":"host","Flags":128,"FlagsString":"permanent","ConfigState":"configured"}],"Routes":[{"Family":10,"Destination":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],"DestinationPrefixLength":128,"TOS":0,"Scope":0,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"global","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[127,255,255,255],"DestinationPrefixLength":32,"PreferredSource":[127,0,0,1],"TOS":0,"Scope":253,"Protocol":2,"Type":3,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"link","ProtocolString":"kernel","TypeString":"broadcast","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[127,0,0,1],"DestinationPrefixLength":32,"PreferredSource":[127,0,0,1],"TOS":0,"Scope":254,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"host","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[127,0,0,0],"DestinationPrefixLength":8,"PreferredSource":[127,0,0,1],"TOS":0,"Scope":254,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"host","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"}]},{"Index":77,"Name":"eth0","Kind":"veth","Type":"ether","Driver":"veth","Flags":69699,"FlagsString":"up,broadcast,running,multicast,lower-up","KernelOperationalState":6,"KernelOperationalStateString":"up","MTU":1500,"MinimumMTU":68,"MaximumMTU":65535,"HardwareAddress":[142,99,82,107,234,232],"BroadcastAddress":[255,255,255,255,255,255],"IPv6LinkLocalAddress":[254,128,0,0,0,0,0,0,140,99,82,255,254,107,234,232],"AdministrativeState":"configured","OperationalState":"routable","CarrierState":"carrier","AddressState":"routable","IPv4AddressState":"routable","IPv6AddressState":"routable","OnlineState":"online","NetworkFile":"/etc/systemd/network/eth0.network","NetworkFileDropins":[],"RequiredForOnline":true,"RequiredOperationalStateForOnline":[null,null],"RequiredFamilyForOnline":"any","ActivationPolicy":"up","DNS":[{"Family":2,"Address":[192,168,1,1],"ConfigSource":"DHCPv4","ConfigProvider":[192,168,1,1]},{"Family":10,"Address":[38,0,64,64,166,193,74,0,0,0,0,0,0,0,0,1],"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,62,189,197,255,254,43,189,153]}],"SearchDomains":[{"Domain":"myfiosgateway.com","ConfigSource":"DHCPv4","ConfigProvider":[192,168,1,1]}],"DNSSettings":[{"LLMNR":"yes","ConfigSource":"static"},{"MDNS":"no","ConfigSource":"static"}],"Addresses":[{"Family":2,"Address":[192,168,1,5],"PrefixLength":24,"ConfigSource":"DHCPv4","ConfigProvider":[192,168,1,1],"Broadcast":[192,168,1,255],"Scope":0,"ScopeString":"global","Flags":0,"FlagsString":null,"PreferredLifetimeUSec":1734964293205,"PreferredLifetimeUsec":1734964293205,"ValidLifetimeUSec":1734964293205,"ValidLifetimeUsec":1734964293205,"ConfigState":"configured"},{"Family":10,"Address":[254,128,0,0,0,0,0,0,140,99,82,255,254,107,234,232],"PrefixLength":64,"ConfigSource":"foreign","Scope":253,"ScopeString":"link","Flags":128,"FlagsString":"permanent","ConfigState":"configured"},{"Family":10,"Address":[38,0,64,64,166,193,74,0,140,99,82,255,254,107,234,232],"PrefixLength":64,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,62,189,197,255,254,43,189,153],"Scope":0,"ScopeString":"global","Flags":768,"FlagsString":"manage-temporary-address,no-prefixroute","PreferredLifetimeUSec":1677495569859,"PreferredLifetimeUsec":1677495569859,"ValidLifetimeUSec":1677495569859,"ValidLifetimeUsec":1677495569859,"ConfigState":"configured"}],"NextHops":[{"ID":1635324079,"Family":10,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,62,189,197,255,254,43,189,153],"Gateway":[254,128,0,0,0,0,0,0,62,189,197,255,254,43,189,153],"Flags":0,"FlagsString":"","Protocol":9,"ProtocolString":"9","Blackhole":false,"ConfigState":"configured"}],"Routes":[{"Family":2,"Destination":[192,168,1,0],"DestinationPrefixLength":24,"PreferredSource":[192,168,1,5],"TOS":0,"Scope":253,"Protocol":2,"Type":1,"Priority":1024,"Table":254,"Flags":0,"ConfigSource":"foreign","ScopeString":"link","ProtocolString":"kernel","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[192,168,1,255],"DestinationPrefixLength":32,"PreferredSource":[192,168,1,5],"TOS":0,"Scope":253,"Protocol":2,"Type":3,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"link","ProtocolString":"kernel","TypeString":"broadcast","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":10,"Destination":[38,0,64,64,166,193,74,0,140,99,82,255,254,107,234,232],"DestinationPrefixLength":128,"TOS":0,"Scope":0,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"global","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":10,"Destination":[38,0,64,64,166,193,74,0,0,0,0,0,0,0,0,0],"DestinationPrefixLength":64,"TOS":0,"Scope":0,"Protocol":9,"Type":1,"Priority":1024,"Table":254,"Flags":0,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,62,189,197,255,254,43,189,153],"ScopeString":"global","ProtocolString":"9","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","LifetimeUSec":1677495568830,"ConfigState":"configured"},{"Family":10,"Destination":[254,128,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"DestinationPrefixLength":64,"TOS":0,"Scope":0,"Protocol":2,"Type":1,"Priority":256,"Table":254,"Flags":0,"ConfigSource":"foreign","ScopeString":"global","ProtocolString":"kernel","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[0,0,0,0],"DestinationPrefixLength":0,"Gateway":[192,168,1,1],"PreferredSource":[192,168,1,5],"TOS":0,"Scope":0,"Protocol":16,"Type":1,"Priority":1024,"Table":254,"Flags":0,"ConfigSource":"DHCPv4","ConfigProvider":[192,168,1,1],"ScopeString":"global","ProtocolString":"16","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[192,168,1,1],"DestinationPrefixLength":32,"PreferredSource":[192,168,1,5],"TOS":0,"Scope":253,"Protocol":16,"Type":1,"Priority":1024,"Table":254,"Flags":0,"ConfigSource":"DHCPv4","ConfigProvider":[192,168,1,1],"ScopeString":"link","ProtocolString":"16","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[192,168,1,5],"DestinationPrefixLength":32,"PreferredSource":[192,168,1,5],"TOS":0,"Scope":254,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"host","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":10,"Destination":[255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"DestinationPrefixLength":8,"TOS":0,"Scope":0,"Protocol":2,"Type":5,"Priority":256,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"global","ProtocolString":"kernel","TypeString":"multicast","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":10,"Destination":[254,128,0,0,0,0,0,0,140,99,82,255,254,107,234,232],"DestinationPrefixLength":128,"TOS":0,"Scope":0,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"global","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"}],"DHCPv4Client":{"Lease":{"LeaseTimestampUSec":1648564292241,"Timeout1USec":1691764292241,"Timeout2USec":1724164292241},"ClientIdentifier":[1,142,99,82,107,234,232]},"DHCPv6Client":{"Lease":{"LeaseTimestampUSec":958440922609},"DUID":[0,2,0,0,171,17,185,66,21,165,25,232,202,84]}},{"Index":79,"Name":"eth1","Kind":"veth","Type":"ether","Driver":"veth","Flags":69699,"FlagsString":"up,broadcast,running,multicast,lower-up","KernelOperationalState":6,"KernelOperationalStateString":"up","MTU":1500,"MinimumMTU":68,"MaximumMTU":65535,"HardwareAddress":[142,99,82,107,234,142],"BroadcastAddress":[255,255,255,255,255,255],"IPv6LinkLocalAddress":[254,128,0,0,0,0,0,0,140,99,82,255,254,107,234,142],"AdministrativeState":"configured","OperationalState":"routable","CarrierState":"carrier","AddressState":"routable","IPv4AddressState":"routable","IPv6AddressState":"routable","OnlineState":"online","NetworkFile":"/etc/systemd/network/50-eth1.network","NetworkFileDropins":[],"RequiredForOnline":true,"RequiredOperationalStateForOnline":[null,null],"RequiredFamilyForOnline":"any","ActivationPolicy":"up","DNS":[{"Family":10,"Address":[253,66,163,4,200,54,42,127,0,0,0,0,0,0,0,1],"ConfigSource":"DHCPv6","ConfigProvider":[254,128,0,0,0,0,0,0,18,102,106,255,254,118,188,91]},{"Family":10,"Address":[253,66,163,4,200,54,42,127,0,0,0,0,0,0,0,1],"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,18,102,106,255,254,118,188,91]}],"DNSSettings":[{"LLMNR":"yes","ConfigSource":"static"},{"MDNS":"no","ConfigSource":"static"}],"Addresses":[{"Family":10,"Address":[254,128,0,0,0,0,0,0,140,99,82,255,254,107,234,142],"PrefixLength":64,"ConfigSource":"foreign","Scope":253,"ScopeString":"link","Flags":128,"FlagsString":"permanent","ConfigState":"configured"},{"Family":2,"Address":[10,4,20,1],"PrefixLength":24,"ConfigSource":"static","Broadcast":[10,4,20,255],"Scope":0,"ScopeString":"global","Flags":128,"FlagsString":"permanent","ConfigState":"configured"},{"Family":10,"Address":[253,66,163,4,200,54,42,127,140,99,82,255,254,107,234,142],"PrefixLength":64,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,18,102,106,255,254,118,188,91],"Scope":0,"ScopeString":"global","Flags":896,"FlagsString":"permanent,manage-temporary-address,no-prefixroute","ConfigState":"configured"}],"NextHops":[{"ID":3144860678,"Family":10,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,18,102,106,255,254,118,188,91],"Gateway":[254,128,0,0,0,0,0,0,18,102,106,255,254,118,188,91],"Flags":0,"FlagsString":"","Protocol":9,"ProtocolString":"9","Blackhole":false,"ConfigState":"configured"}],"Routes":[{"Family":2,"Destination":[10,4,20,255],"DestinationPrefixLength":32,"PreferredSource":[10,4,20,1],"TOS":0,"Scope":253,"Protocol":2,"Type":3,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"link","ProtocolString":"kernel","TypeString":"broadcast","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":10,"Destination":[254,128,0,0,0,0,0,0,140,99,82,255,254,107,234,142],"DestinationPrefixLength":128,"TOS":0,"Scope":0,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"global","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":10,"Destination":[253,66,163,4,200,54,42,127,0,0,0,0,0,0,0,0],"DestinationPrefixLength":64,"TOS":0,"Scope":0,"Protocol":9,"Type":1,"Priority":1024,"Table":254,"Flags":0,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,18,102,106,255,254,118,188,91],"ScopeString":"global","ProtocolString":"9","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":10,"Destination":[253,66,163,4,200,54,42,127,140,99,82,255,254,107,234,142],"DestinationPrefixLength":128,"TOS":0,"Scope":0,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"global","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[10,4,20,1],"DestinationPrefixLength":32,"PreferredSource":[10,4,20,1],"TOS":0,"Scope":254,"Protocol":2,"Type":2,"Priority":0,"Table":255,"Flags":0,"ConfigSource":"foreign","ScopeString":"host","ProtocolString":"kernel","TypeString":"local","TableString":"local","Preference":0,"FlagsString":"","ConfigState":"configured"},{"Family":2,"Destination":[10,4,20,0],"DestinationPrefixLength":24,"PreferredSource":[10,4,20,1],"TOS":0,"Scope":253,"Protocol":2,"Type":1,"Priority":0,"Table":254,"Flags":0,"ConfigSource":"foreign","ScopeString":"link","ProtocolString":"kernel","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","ConfigState":"configured"}],"DHCPv6Client":{"Lease":{"LeaseTimestampUSec":1585104388996},"DUID":[0,2,0,0,171,17,185,66,21,165,25,232,202,84]}}],"Routes":[{"Family":10,"Destination":[38,0,64,64,166,193,74,0,0,0,0,0,0,0,0,0],"DestinationPrefixLength":56,"TOS":0,"Scope":0,"Protocol":9,"Type":1,"Priority":512,"Table":254,"Flags":0,"NextHopID":1635324079,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,62,189,197,255,254,43,189,153],"ScopeString":"global","ProtocolString":"9","TypeString":"unicast","TableString":"main","Preference":1,"FlagsString":"","LifetimeUSec":1677495568830,"ConfigState":"configured"},{"Family":10,"Destination":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"DestinationPrefixLength":0,"TOS":0,"Scope":0,"Protocol":9,"Type":1,"Priority":1024,"Table":254,"Flags":0,"NextHopID":3144860678,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,18,102,106,255,254,118,188,91],"ScopeString":"global","ProtocolString":"9","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","LifetimeUSec":1671685709586,"ConfigState":"configured"},{"Family":10,"Destination":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"DestinationPrefixLength":0,"TOS":0,"Scope":0,"Protocol":9,"Type":1,"Priority":1024,"Table":254,"Flags":0,"NextHopID":1635324079,"ConfigSource":"NDisc","ConfigProvider":[254,128,0,0,0,0,0,0,62,189,197,255,254,43,189,153],"ScopeString":"global","ProtocolString":"9","TypeString":"unicast","TableString":"main","Preference":0,"FlagsString":"","LifetimeUSec":1671195568830,"ConfigState":"configured"}],"RoutingPolicyRules":[{"Family":10,"Protocol":2,"ProtocolString":"kernel","TOS":0,"Type":1,"TypeString":"table","IPProtocol":0,"IPProtocolString":"ip","Priority":0,"FirewallMark":0,"FirewallMask":0,"Table":255,"TableString":"local","Invert":false,"ConfigSource":"foreign","ConfigState":"configured"},{"Family":10,"Protocol":2,"ProtocolString":"kernel","TOS":0,"Type":1,"TypeString":"table","IPProtocol":0,"IPProtocolString":"ip","Priority":32766,"FirewallMark":0,"FirewallMask":0,"Table":254,"TableString":"main","Invert":false,"ConfigSource":"foreign","ConfigState":"configured"},{"Family":2,"Protocol":2,"ProtocolString":"kernel","TOS":0,"Type":1,"TypeString":"table","IPProtocol":0,"IPProtocolString":"ip","Priority":32767,"FirewallMark":0,"FirewallMask":0,"Table":253,"TableString":"default","Invert":false,"ConfigSource":"foreign","ConfigState":"configured"},{"Family":2,"Protocol":2,"ProtocolString":"kernel","TOS":0,"Type":1,"TypeString":"table","IPProtocol":0,"IPProtocolString":"ip","Priority":0,"FirewallMark":0,"FirewallMask":0,"Table":255,"TableString":"local","Invert":false,"ConfigSource":"foreign","ConfigState":"configured"},{"Family":2,"Protocol":2,"ProtocolString":"kernel","TOS":0,"Type":1,"TypeString":"table","IPProtocol":0,"IPProtocolString":"ip","Priority":32766,"FirewallMark":0,"FirewallMask":0,"Table":254,"TableString":"main","Invert":false,"ConfigSource":"foreign","ConfigState":"configured"}]}. This provides more reliable and maintainable runtime state extraction.

lib/network.py:
- Rewrite parse_networkctl_status() to parse JSON instead of text lines
- Add _bytes_to_ip() helper for converting address byte arrays to IP strings
- Extract addresses, gateway (from Routes), DNS, MAC, and state from structured JSON
- Add proper error handling for malformed JSON input

daemon/handlers/network.py:
- Update all 3 callers (get_interfaces, get_interface, save_interface) to use --json=short
- Fix get_interfaces to include runtime-only interfaces by unioning config and runtime names (minus lo), rather than only iterating config-defined interfaces

lib/state.py:
- Update _collect_networkd to use --json=short flag

tests/test_network.py, tests/test_network_integration.py:
- Update all test fixtures from text output to matching JSON structure
2026-06-16 04:45:59 +00:00

339 lines
11 KiB
Python

"""Integration tests for networkd interactions with other subsystems.
Tests TF-8 (DNS upstream sync), TF-9 (DHCP range inference), TF-10 (zone inference).
"""
import json
from pathlib import Path
from unittest.mock import patch
import pytest
from lib import network as _net
@pytest.fixture
def tmp_network(tmp_path):
orig_config = _net.CONFIG_FILE
orig_data = _net.DATA_DIR
_net.CONFIG_FILE = tmp_path / "config" / "network" / "config.json"
_net.DATA_DIR = tmp_path / "data" / "networkd"
yield tmp_path
_net.CONFIG_FILE = orig_config
_net.DATA_DIR = orig_data
# =================================================================
# TF-13: Integration tests — DNS upstream sync
# =================================================================
class TestDnsUpstreamIntegration:
"""collect_upstream_dns + set_upstreams integration."""
def test_wan_dns_becomes_dnsmasq_upstream(self, tmp_network):
"""WAN interface with public DNS should produce upstream list."""
cfg = _net.get_config()
cfg["interfaces"] = {
"eth0": {
"dhcp": "ipv4",
"dns": ["8.8.8.8", "1.1.1.1"],
},
"lan0": {
"addresses": [{"address": "192.168.1.1/24"}],
"dns": ["127.0.0.1"],
},
}
_net.save_config(cfg)
upstreams = _net.collect_upstream_dns(cfg)
assert "8.8.8.8" in upstreams
assert "1.1.1.1" in upstreams
assert "127.0.0.1" not in upstreams
def test_all_dns_local_yields_empty(self, tmp_network):
"""When all DNS servers are local, no upstreams."""
cfg = {
"interfaces": {
"lan0": {
"addresses": [{"address": "192.168.1.1/24"}],
"dns": ["127.0.0.1", "192.168.1.1"],
"ipv6_dns": ["fe80::1"],
}
}
}
_net.save_config(cfg)
upstreams = _net.collect_upstream_dns(cfg)
assert upstreams == []
def test_mixed_v4_v6_upstreams(self, tmp_network):
"""Collects both IPv4 and IPv6 public DNS."""
cfg = {
"interfaces": {
"eth0": {
"dns": ["8.8.8.8"],
"ipv6_dns": ["2001:4860:4860::8888"],
}
}
}
_net.save_config(cfg)
upstreams = _net.collect_upstream_dns(cfg)
assert "8.8.8.8" in upstreams
assert "2001:4860:4860::8888" in upstreams
def test_upstream_dns_after_generate_files(self, tmp_network):
"""Full flow: save config -> generate files -> collect upstreams."""
cfg = {
"interfaces": {
"eth0": {
"dhcp": "ipv4",
"dns": ["8.8.8.8", "192.168.1.1"],
"gateway": "10.0.0.254",
}
}
}
_net.save_config(cfg)
result = _net.generate_network_files(cfg)
assert len(result["generated"]) == 1
upstreams = _net.collect_upstream_dns(cfg)
assert "8.8.8.8" in upstreams
assert "192.168.1.1" not in upstreams
# =================================================================
# TF-13: Integration tests — DHCP range inference
# =================================================================
class TestDhcpRangesIntegration:
"""infer_dhcp_ranges for multiple interface scenarios."""
def test_multi_interface_ranges(self, tmp_network):
"""Each interface with static IP gets its own range."""
cfg = {
"interfaces": {
"lan1": {
"addresses": [{"address": "192.168.1.1/24"}],
},
"lan2": {
"addresses": [{"address": "10.10.0.1/16"}],
},
"wan0": {
"dhcp": "ipv4",
},
}
}
_net.save_config(cfg)
ranges = _net.infer_dhcp_ranges(cfg)
assert "lan1" in ranges
assert "lan2" in ranges
assert "wan0" not in ranges
assert ranges["lan1"]["start"] == "192.168.1.100"
assert ranges["lan1"]["end"] == "192.168.1.200"
assert ranges["lan2"]["start"] == "10.10.0.100"
assert ranges["lan2"]["end"] == "10.10.0.200"
def test_generate_then_infer(self, tmp_network):
"""End-to-end: save, generate, infer ranges."""
cfg = {
"interfaces": {
"eth0": {
"addresses": [{"address": "172.16.0.1/24"}],
"gateway": "172.16.0.254",
"dns": ["8.8.8.8"],
}
}
}
_net.save_config(cfg)
_net.generate_network_files(cfg)
ranges = _net.infer_dhcp_ranges(cfg)
assert "eth0" in ranges
assert ranges["eth0"]["subnet"] == "172.16.0.0"
assert ranges["eth0"]["prefix"] == 24
def test_ranges_cross_reference_with_zones(self, tmp_network):
"""DHCP ranges for LAN interfaces correlate with zone inference."""
cfg = {
"interfaces": {
"lan0": {
"addresses": [{"address": "192.168.1.1/24"}],
},
"wan0": {
"dhcp": "ipv4",
},
}
}
_net.save_config(cfg)
ranges = _net.infer_dhcp_ranges(cfg)
zones = _net.infer_zones(cfg)
assert "lan0" in ranges
assert zones["lan0"] == "lan"
assert "wan0" not in ranges
assert zones["wan0"] == "wan"
# =================================================================
# TF-13: Integration tests — zone inference with networkd config
# =================================================================
class TestZonesIntegration:
"""infer_zones with realistic networkd configurations."""
def test_typical_router_setup(self, tmp_network):
"""WAN (DHCP), LAN (static), WG (WireGuard) zones."""
cfg = {
"interfaces": {
"eth0": {
"dhcp": "ipv4",
},
"eth1": {
"addresses": [{"address": "192.168.1.1/24"}],
"gateway": "192.168.1.254",
},
"wg0": {
"addresses": [{"address": "10.137.0.1/24"}],
},
"br-mgmt": {
"addresses": [{"address": "10.0.0.1/24"}],
"routes": [
{"destination": "0.0.0.0/0", "gateway": "10.0.0.254"},
],
},
}
}
_net.save_config(cfg)
zones = _net.infer_zones(cfg)
assert zones["eth0"] == "wan"
assert zones["eth1"] == "lan"
assert zones["wg0"] == "wan"
assert zones["br-mgmt"] == "management"
def test_zone_inference_after_generate(self, tmp_network):
"""Zone inference works after generate_network_files."""
cfg = {
"interfaces": {
"wan0": {"dhcp": "ipv4"},
"lan0": {"addresses": [{"address": "192.168.10.1/24"}]},
}
}
_net.save_config(cfg)
_net.generate_network_files(cfg)
zones = _net.infer_zones(cfg)
assert zones["wan0"] == "wan"
assert zones["lan0"] == "lan"
def test_full_pipeline(self, tmp_network):
"""Full pipeline: config -> generate -> DNS -> ranges -> zones."""
cfg = {
"interfaces": {
"eth0": {
"dhcp": "ipv4",
"dns": ["8.8.8.8", "1.1.1.1", "192.168.1.1"],
},
"eth1": {
"addresses": [{"address": "192.168.1.1/24"}],
"dns": ["127.0.0.1"],
},
"wg0": {
"addresses": [{"address": "10.137.0.1/24"}],
},
}
}
_net.save_config(cfg)
gen_result = _net.generate_network_files(cfg)
assert len(gen_result["generated"]) == 3
upstreams = _net.collect_upstream_dns(cfg)
assert "8.8.8.8" in upstreams
assert "1.1.1.1" in upstreams
assert "192.168.1.1" not in upstreams
assert "127.0.0.1" not in upstreams
ranges = _net.infer_dhcp_ranges(cfg)
assert "eth1" in ranges
assert "eth0" not in ranges
# wg0 has a static address so it also gets a candidate range
assert "wg0" in ranges
zones = _net.infer_zones(cfg)
assert zones["eth0"] == "wan"
assert zones["eth1"] == "lan"
assert zones["wg0"] == "wan"
# =================================================================
# TF-11: state.py parser dedup verification
# =================================================================
class TestStateParserDedup:
"""Verify lib/state.py uses lib.network.parse_networkctl_status()."""
def test_state_uses_network_parser(self):
"""The networkd collector in state.py should import from lib.network."""
import lib.state as _state
source = Path(_state.__file__).read_text()
assert "from lib.network import parse_networkctl_status" in source
assert "parse_networkctl_status" in source
def test_networkd_collector_returns_correct_format(self):
"""_collect_networkd should return interfaces dict + timestamp."""
import lib.state as _state
with patch("lib.state.run") as mock_run:
mock_run.return_value = json.dumps(
{
"Interfaces": [
{
"Name": "eth0",
"Type": "ether",
"OperationalState": "routable",
"Addresses": [
{
"Family": 2,
"Address": [10, 0, 0, 1],
"PrefixLength": 24,
}
],
"DNS": [
{"Family": 2, "Address": [8, 8, 8, 8]},
],
"Routes": [
{
"Family": 2,
"Destination": [0, 0, 0, 0],
"DestinationPrefixLength": 0,
"Gateway": [10, 0, 0, 254],
}
],
}
]
}
)
result = _state._collect_networkd()
assert "interfaces" in result
assert "timestamp" in result
assert "eth0" in result["interfaces"]
assert "10.0.0.1/24" in result["interfaces"]["eth0"]["addresses"]
def test_networkd_collector_handles_failure(self):
"""_collect_networkd returns empty interfaces on error."""
import lib.state as _state
with patch("lib.state.run", side_effect=RuntimeError("no networkctl")):
result = _state._collect_networkd()
assert result["interfaces"] == {}
assert "timestamp" in result