Skip to content

Commit

Permalink
state/status: add support for VRF members and interface kind
Browse files Browse the repository at this point in the history
"netplan status" will now also show information about what interfaces are
members of a VRF and for each interface show what is the VRF it's
associated to.

It will also look for the Kind property to be more specific about the
interface type. For example, instead of showing a dummy interface as
being an ethernet device it will show dummy.
  • Loading branch information
daniloegea committed Jan 26, 2024
1 parent 3d691a9 commit 5399f4f
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 23 deletions.
7 changes: 7 additions & 0 deletions netplan_cli/cli/commands/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,13 @@ def pretty_print(self, data: JSON, total: int, _console_width=None) -> None:
value=val,
))

val = data.get('vrf')
if val:
pprint(('{title:>'+pad+'} {value}').format(
title='VRF:',
value=val,
))

lst = data.get('members', [])
for i, val in enumerate(lst):
pprint(('{title:>'+pad+'} {value}').format(
Expand Down
32 changes: 22 additions & 10 deletions netplan_cli/cli/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,19 @@
DEVICE_TYPES = {
'bond': 'bond',
'bridge': 'bridge',
'dummy': 'dummy',
'ether': 'ethernet',
'ipgre': 'tunnel',
'ip6gre': 'tunnel',
'loopback': 'ethernet',
'sit': 'tunnel',
'tunnel': 'tunnel',
'tun': 'tunnel',
'tunnel6': 'tunnel',
'wireguard': 'tunnel',
'wlan': 'wifi',
'wwan': 'modem',
'veth': 'veth',
'vlan': 'vlan',
'vrf': 'vrf',
'vxlan': 'tunnel',
Expand Down Expand Up @@ -86,6 +89,7 @@ def __init__(self, ip: dict, nd_data: JSON = [], nm_data: JSON = [],
self.macaddress: str = self.__extract_mac(ip)
self.bridge: str = None
self.bond: str = None
self.vrf: str = None
self.members: List[str] = []

# Filter networkd/NetworkManager data
Expand Down Expand Up @@ -220,6 +224,8 @@ def json(self) -> JSON:
json['bridge'] = self.bridge
if self.bond:
json['bond'] = self.bond
if self.vrf:
json['vrf'] = self.vrf
if self.members:
json['members'] = self.members
return (self.name, json)
Expand All @@ -235,6 +241,10 @@ def down(self) -> bool:
@property
def type(self) -> str:
nd_type = self.nd.get('Type') if self.nd else None
if nd_type == 'ether':
# There are different kinds of 'ether' devices, such as VRFs, veth and dummies
if kind := self.nd.get('Kind'):
nd_type = kind
if device_type := DEVICE_TYPES.get(nd_type):
return device_type
logging.warning('Unknown device type: {}'.format(nd_type))
Expand Down Expand Up @@ -337,7 +347,7 @@ def __init__(self, ifname=None, all=False):
self.interface_list = [Interface(itf, networkd, nmcli, (dns_addresses, dns_search),
(route4, route6)) for itf in iproute2]

# get bridge/bond data
# get bridge/bond/vrf data
self.correlate_members_and_uplink(self.interface_list)

# show only active interfaces by default
Expand Down Expand Up @@ -523,24 +533,26 @@ def query_members(cls, ifname: str) -> List[str]:

@classmethod
def correlate_members_and_uplink(cls, interfaces: List[Interface]) -> None:
uplink_types = ['bond', 'bridge', 'vrf']
members_to_uplink = {}
uplink_to_members = defaultdict(list)
for interface in filter(lambda i: i.type in ['bond', 'bridge'], interfaces):
for interface in filter(lambda i: i.type in uplink_types, interfaces):
members = cls.query_members(interface.name)
for member in members:
member_tuple = namedtuple('Member', ['name', 'type'])
members_to_uplink[member] = member_tuple(interface.name, interface.type)
uplink_to_members[interface.name] = members

for interface in interfaces:
if interface.type not in ['bridge', 'bond']:
if uplink := members_to_uplink.get(interface.name):
if uplink.type == 'bridge':
interface.bridge = uplink.name
if uplink.type == 'bond':
interface.bond = uplink.name

if interface.type in ['bridge', 'bond']:
if uplink := members_to_uplink.get(interface.name):
if uplink.type == 'bridge':
interface.bridge = uplink.name
if uplink.type == 'bond':
interface.bond = uplink.name
if uplink.type == 'vrf':
interface.vrf = uplink.name

if interface.type in uplink_types:
if members := uplink_to_members.get(interface.name):
interface.members = members

Expand Down
29 changes: 18 additions & 11 deletions tests/cli/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ def test_query_networkd(self, mock):
mock.return_value = NETWORKD
res = SystemConfigState.query_networkd()
mock.assert_called_with(['networkctl', '--json=short'], text=True)
self.assertEqual(len(res), 8)
self.assertEqual(len(res), 9)
self.assertListEqual([itf.get('Name') for itf in res],
['lo', 'enp0s31f6', 'wlan0', 'wg0', 'wwan0', 'tun0', 'mybr0', 'mybond0'])
['lo', 'enp0s31f6', 'wlan0', 'wg0', 'wwan0', 'tun0', 'mybr0', 'mybond0', 'myvrf0'])

@patch('subprocess.check_output')
def test_query_networkd_fail(self, mock):
Expand Down Expand Up @@ -258,16 +258,9 @@ def test_query_members_fail(self, mock):
self.assertListEqual(bridge, [])
self.assertIn('WARNING:root:Cannot query bridge:', cm.output[0])

@classmethod
def mock_query_members(cls, interface):
if interface == 'br0':
return ['eth0', 'eth1']
if interface == 'bond0':
return ['eth2', 'eth3']

@patch('netplan_cli.cli.state.SystemConfigState.query_members')
def test_correlate_members_and_uplink_bridge(self, mock):
mock.side_effect = self.mock_query_members
mock.side_effect = lambda _: ['eth0', 'eth1']
interface1 = Interface({'ifname': 'eth0'})
interface1.nd = {'Type': 'ether'}
interface2 = Interface({'ifname': 'eth1'})
Expand All @@ -281,7 +274,7 @@ def test_correlate_members_and_uplink_bridge(self, mock):

@patch('netplan_cli.cli.state.SystemConfigState.query_members')
def test_correlate_members_and_uplink_bond(self, mock):
mock.side_effect = self.mock_query_members
mock.side_effect = lambda _: ['eth2', 'eth3']
interface1 = Interface({'ifname': 'eth2'})
interface1.nd = {'Type': 'ether'}
interface2 = Interface({'ifname': 'eth3'})
Expand All @@ -293,6 +286,20 @@ def test_correlate_members_and_uplink_bond(self, mock):
self.assertEqual(interface2.bond, 'bond0')
self.assertListEqual(interface3.members, ['eth2', 'eth3'])

@patch('netplan_cli.cli.state.SystemConfigState.query_members')
def test_correlate_members_and_uplink_vrf(self, mock):
mock.side_effect = lambda _: ['eth2', 'eth3']
interface1 = Interface({'ifname': 'eth2'})
interface1.nd = {'Type': 'ether'}
interface2 = Interface({'ifname': 'eth3'})
interface2.nd = {'Type': 'ether'}
interface3 = Interface({'ifname': 'vrf0'})
interface3.nd = {'Type': 'ether', 'Kind': 'vrf'}
SystemConfigState.correlate_members_and_uplink([interface1, interface2, interface3])
self.assertEqual(interface1.vrf, 'vrf0')
self.assertEqual(interface2.vrf, 'vrf0')
self.assertListEqual(interface3.members, ['eth2', 'eth3'])


class TestNetplanState(unittest.TestCase):
'''Test netplan state NetplanConfigState class'''
Expand Down
Loading

0 comments on commit 5399f4f

Please sign in to comment.