Add VLAN support for nmstate provider

It includes support for VLANs over type interface, ovs bridge,
linux bonds

Change-Id: I2c0cec5918f680a3fff7e22126b06441185f7975
This commit is contained in:
Karthik S 2023-06-27 18:19:48 +05:30
parent 24c8cd3266
commit c70bb48e57
2 changed files with 326 additions and 0 deletions

View File

@ -32,6 +32,7 @@ from libnmstate.schema import OvsDB
from libnmstate.schema import OVSInterface
from libnmstate.schema import Route as NMRoute
from libnmstate.schema import RouteRule as NMRouteRule
from libnmstate.schema import VLAN
import logging
import netaddr
import re
@ -209,6 +210,7 @@ class NmstateNetConfig(os_net_config.NetConfig):
def __init__(self, noop=False, root_dir=''):
super(NmstateNetConfig, self).__init__(noop, root_dir)
self.interface_data = {}
self.vlan_data = {}
self.route_data = {}
self.rules_data = []
self.dns_data = {'server': [], 'domain': []}
@ -483,6 +485,18 @@ class NmstateNetConfig(os_net_config.NetConfig):
}
}
bps.append(port)
# if the member is of type interface but a vlan
# like eth0.605
elif re.match(r'\w+\.\d+$', member):
vlan_id = int(member.split('.')[1])
port = {
OVSBridge.Port.NAME: member,
OVSBridge.Port.VLAN_SUBTREE: {
OVSBridge.Port.Vlan.MODE: 'access',
OVSBridge.Port.Vlan.TAG: vlan_id
}
}
bps.append(port)
else:
port = {'name': member}
bps.append(port)
@ -858,6 +872,23 @@ class NmstateNetConfig(os_net_config.NetConfig):
:param interface: The Interface object to add.
"""
if re.match(r'\w+\.\d+$', interface.name):
vlan_id = int(interface.name.split('.')[1])
device = interface.name.split('.')[0]
vlan_port = objects.Vlan(
device, vlan_id,
use_dhcp=interface.use_dhcp, use_dhcpv6=interface.use_dhcpv6,
addresses=interface.addresses, routes=interface.routes,
rules=interface.rules, mtu=interface.mtu,
primary=interface.primary, nic_mapping=None,
persist_mapping=None, defroute=interface.defroute,
dhclient_args=interface.dhclient_args,
dns_servers=interface.dns_servers, nm_controlled=True,
onboot=interface.onboot, domain=interface.domain)
vlan_port.name = interface.name
self.add_vlan(vlan_port)
return
logger.info(f'adding interface: {interface.name}')
data = self._add_common(interface)
if isinstance(interface, objects.Interface):
@ -878,6 +909,32 @@ class NmstateNetConfig(os_net_config.NetConfig):
logger.debug(f'interface data: {data}')
self.interface_data[interface.name] = data
def add_vlan(self, vlan):
"""Add a Vlan object to the net config object.
:param vlan: The vlan object to add.
"""
logger.info(f'adding vlan: {vlan.name}')
data = self._add_common(vlan)
if vlan.device:
base_iface = vlan.device
elif vlan.linux_bond_name:
base_iface = vlan.linux_bond_name
if vlan.bridge_name:
# Handle the VLANs for ovs bridges
# vlans on OVS bridges are internal ports (no device, etc)
data[Interface.TYPE] = InterfaceType.OVS_INTERFACE
else:
data[Interface.TYPE] = InterfaceType.VLAN
data[VLAN.CONFIG_SUBTREE] = {}
data[VLAN.CONFIG_SUBTREE][VLAN.ID] = vlan.vlan_id
data[VLAN.CONFIG_SUBTREE][VLAN.BASE_IFACE] = base_iface
logger.debug(f'vlan data: {data}')
self.vlan_data[vlan.name] = data
def _ovs_extra_cfg_eq_val(self, ovs_extra, cmd_map, data):
index = 0
logger.info(f'Current ovs_extra {ovs_extra}')
@ -1335,6 +1392,17 @@ class NmstateNetConfig(os_net_config.NetConfig):
logger.info('Routes_data %s' % routes_data)
apply_routes.extend(routes_data)
for vlan_name, vlan_data in self.vlan_data.items():
vlan_state = self.iface_state(vlan_name)
if not is_dict_subset(vlan_state, vlan_data):
updated_interfaces[vlan_name] = vlan_data
else:
logger.info('No changes required for vlan interface: %s' %
vlan_name)
routes_data = self.generate_routes(vlan_name)
logger.info('Routes_data %s' % routes_data)
apply_routes.extend(routes_data)
if activate:
if not self.noop:
try:
@ -1374,5 +1442,6 @@ class NmstateNetConfig(os_net_config.NetConfig):
self.interface_data = {}
self.bridge_data = {}
self.linuxbond_data = {}
self.vlan_data = {}
return updated_interfaces

View File

@ -184,6 +184,9 @@ class TestNmstateNetConfig(base.TestCase):
def get_interface_config(self, name='em1'):
return self.provider.interface_data[name]
def get_vlan_config(self, name):
return self.provider.vlan_data[name]
def get_bridge_config(self, name):
return self.provider.bridge_data[name]
@ -957,6 +960,260 @@ class TestNmstateNetConfig(base.TestCase):
self.assertCountEqual(yaml.safe_load(expected_bond0_config),
self.get_linuxbond_config('bond0'))
def test_vlan_interface(self):
expected_vlan1_cfg = """
name: vlan502
type: vlan
vlan:
base-iface: em2
id: 502
state: up
ipv4:
dhcp: false
enabled: false
ipv6:
address:
- ip: "2001:abc:a::"
prefix-length: 64
autoconf: false
dhcp: false
enabled: true
"""
v6_addr = objects.Address('2001:abc:a::/64')
vlan1 = objects.Vlan('em2', 502, addresses=[v6_addr])
self.provider.add_vlan(vlan1)
self.assertEqual(yaml.safe_load(expected_vlan1_cfg),
self.get_vlan_config('vlan502'))
def test_vlan_as_interface(self):
expected_vlan1_cfg = """
name: em2.502
type: vlan
vlan:
base-iface: em2
id: 502
state: up
ipv4:
dhcp: false
enabled: false
ipv6:
address:
- ip: "2001:abc:a::"
prefix-length: 64
autoconf: false
dhcp: false
enabled: true
"""
v6_addr = objects.Address('2001:abc:a::/64')
em2 = objects.Interface('em2.502', addresses=[v6_addr])
self.provider.add_interface(em2)
self.assertEqual(yaml.safe_load(expected_vlan1_cfg),
self.get_vlan_config('em2.502'))
def test_add_vlan_ovs(self):
expected_vlan1_cfg = """
name: vlan5
ipv4:
dhcp: false
enabled: false
ipv6:
autoconf: false
dhcp: false
enabled: false
state: up
type: ovs-interface
"""
expected_bridge_cfg = """
name: br-ctlplane
bridge:
options:
fail-mode: standalone
mcast-snooping-enable: false
rstp: false
stp: false
port:
- name: em2
- name: vlan5
vlan:
mode: access
tag: 5
- name: br-ctlplane-p
ovs-db:
external_ids: {}
other_config: {}
state: up
type: ovs-bridge
"""
interface1 = objects.Interface('em2')
vlan = objects.Vlan(None, 5)
bridge = objects.OvsBridge('br-ctlplane', use_dhcp=True,
members=[interface1, vlan])
self.provider.add_bridge(bridge)
self.provider.add_vlan(vlan)
self.assertEqual(yaml.safe_load(expected_vlan1_cfg),
self.get_vlan_config('vlan5'))
self.assertEqual(yaml.safe_load(expected_bridge_cfg),
self.get_bridge_config('br-ctlplane'))
def test_add_vlan_mtu_1500(self):
expected_vlan1_cfg = """
name: vlan5
type: vlan
vlan:
base-iface: em1
id: 5
state: up
mtu: 1500
ipv4:
dhcp: false
enabled: false
ipv6:
autoconf: false
dhcp: false
enabled: false
"""
vlan = objects.Vlan('em1', 5, mtu=1500)
self.provider.add_vlan(vlan)
self.assertEqual(yaml.safe_load(expected_vlan1_cfg),
self.get_vlan_config('vlan5'))
def test_add_ovs_bridge_with_vlan(self):
expected_vlan1_cfg = """
name: vlan5
ipv4:
dhcp: false
enabled: false
ipv6:
autoconf: false
dhcp: false
enabled: false
state: up
type: ovs-interface
"""
expected_bridge_cfg = """
name: br-ctlplane
bridge:
options:
fail-mode: standalone
mcast-snooping-enable: false
rstp: false
stp: false
port:
- name: vlan5
vlan:
mode: access
tag: 5
- name: br-ctlplane-p
ovs-db:
external_ids: {}
other_config: {}
state: up
type: ovs-bridge
"""
vlan = objects.Vlan('em2', 5)
bridge = objects.OvsBridge('br-ctlplane', use_dhcp=True,
members=[vlan])
self.provider.add_vlan(vlan)
self.provider.add_bridge(bridge)
self.assertEqual(yaml.safe_load(expected_bridge_cfg),
self.get_bridge_config('br-ctlplane'))
self.assertEqual(yaml.safe_load(expected_vlan1_cfg),
self.get_vlan_config('vlan5'))
def test_vlan_over_linux_bond(self):
expected_vlan1_cfg = """
name: vlan5
type: vlan
vlan:
base-iface: bond0
id: 5
state: up
ipv4:
dhcp: false
enabled: false
ipv6:
autoconf: false
dhcp: false
enabled: false
"""
interface1 = objects.Interface('em1', primary=True)
interface2 = objects.Interface('em2')
bond = objects.LinuxBond('bond0', use_dhcp=True,
members=[interface1, interface2])
vlan = objects.Vlan('bond0', 5)
self.provider.add_linux_bond(bond)
self.provider.add_interface(interface1)
self.provider.add_interface(interface2)
self.provider.add_vlan(vlan)
self.assertEqual(yaml.safe_load(expected_vlan1_cfg),
self.get_vlan_config('vlan5'))
def test_add_vlan_route_rules(self):
expected_vlan1_cfg = """
name: vlan5
type: vlan
vlan:
base-iface: em1
id: 5
state: up
ipv4:
dhcp: false
enabled: true
address:
- ip: 192.168.1.2
prefix-length: 24
ipv6:
autoconf: false
dhcp: false
enabled: false
"""
expected_route_table = """
- destination: 172.19.0.0/24
next-hop-address: 192.168.1.1
next-hop-interface: vlan5
table-id: 200
- destination: 172.20.0.0/24
next-hop-address: 192.168.1.1
next-hop-interface: vlan5
table-id: 201
- destination: 172.21.0.0/24
next-hop-address: 192.168.1.1
next-hop-interface: vlan5
table-id: 200
"""
expected_rule = """
- ip-from: 192.0.2.0/24
route-table: 200
"""
route_table1 = objects.RouteTable('table1', 200)
self.provider.add_route_table(route_table1)
route_rule1 = objects.RouteRule('from 192.0.2.0/24 table 200',
'test comment')
# Test route table by name
route1 = objects.Route('192.168.1.1', '172.19.0.0/24', False,
route_table="table1")
# Test that table specified in route_options takes precedence
route2 = objects.Route('192.168.1.1', '172.20.0.0/24', False,
'table 201', route_table=200)
# Test route table specified by integer ID
route3 = objects.Route('192.168.1.1', '172.21.0.0/24', False,
route_table=200)
v4_addr = objects.Address('192.168.1.2/24')
vlan = objects.Vlan('em1', 5, addresses=[v4_addr],
routes=[route1, route2, route3],
rules=[route_rule1])
self.provider.add_vlan(vlan)
self.assertEqual(yaml.safe_load(expected_vlan1_cfg),
self.get_vlan_config('vlan5'))
self.assertEqual(yaml.safe_load(expected_route_table),
self.get_route_config('vlan5'))
self.assertEqual(yaml.safe_load(expected_rule),
self.get_rule_config())
class TestNmstateNetConfigApply(base.TestCase):