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 OVSInterface
from libnmstate.schema import Route as NMRoute from libnmstate.schema import Route as NMRoute
from libnmstate.schema import RouteRule as NMRouteRule from libnmstate.schema import RouteRule as NMRouteRule
from libnmstate.schema import VLAN
import logging import logging
import netaddr import netaddr
import re import re
@ -209,6 +210,7 @@ class NmstateNetConfig(os_net_config.NetConfig):
def __init__(self, noop=False, root_dir=''): def __init__(self, noop=False, root_dir=''):
super(NmstateNetConfig, self).__init__(noop, root_dir) super(NmstateNetConfig, self).__init__(noop, root_dir)
self.interface_data = {} self.interface_data = {}
self.vlan_data = {}
self.route_data = {} self.route_data = {}
self.rules_data = [] self.rules_data = []
self.dns_data = {'server': [], 'domain': []} self.dns_data = {'server': [], 'domain': []}
@ -483,6 +485,18 @@ class NmstateNetConfig(os_net_config.NetConfig):
} }
} }
bps.append(port) 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: else:
port = {'name': member} port = {'name': member}
bps.append(port) bps.append(port)
@ -858,6 +872,23 @@ class NmstateNetConfig(os_net_config.NetConfig):
:param interface: The Interface object to add. :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}') logger.info(f'adding interface: {interface.name}')
data = self._add_common(interface) data = self._add_common(interface)
if isinstance(interface, objects.Interface): if isinstance(interface, objects.Interface):
@ -878,6 +909,32 @@ class NmstateNetConfig(os_net_config.NetConfig):
logger.debug(f'interface data: {data}') logger.debug(f'interface data: {data}')
self.interface_data[interface.name] = 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): def _ovs_extra_cfg_eq_val(self, ovs_extra, cmd_map, data):
index = 0 index = 0
logger.info(f'Current ovs_extra {ovs_extra}') 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) logger.info('Routes_data %s' % routes_data)
apply_routes.extend(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 activate:
if not self.noop: if not self.noop:
try: try:
@ -1374,5 +1442,6 @@ class NmstateNetConfig(os_net_config.NetConfig):
self.interface_data = {} self.interface_data = {}
self.bridge_data = {} self.bridge_data = {}
self.linuxbond_data = {} self.linuxbond_data = {}
self.vlan_data = {}
return updated_interfaces return updated_interfaces

View File

@ -184,6 +184,9 @@ class TestNmstateNetConfig(base.TestCase):
def get_interface_config(self, name='em1'): def get_interface_config(self, name='em1'):
return self.provider.interface_data[name] return self.provider.interface_data[name]
def get_vlan_config(self, name):
return self.provider.vlan_data[name]
def get_bridge_config(self, name): def get_bridge_config(self, name):
return self.provider.bridge_data[name] return self.provider.bridge_data[name]
@ -957,6 +960,260 @@ class TestNmstateNetConfig(base.TestCase):
self.assertCountEqual(yaml.safe_load(expected_bond0_config), self.assertCountEqual(yaml.safe_load(expected_bond0_config),
self.get_linuxbond_config('bond0')) 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): class TestNmstateNetConfigApply(base.TestCase):