Skip to content

Commit

Permalink
[feature] Added support for DSA #195
Browse files Browse the repository at this point in the history
- Added VLAN 802.1q and VLAN 802.1ad interfaces
  • Loading branch information
pandafy committed Mar 30, 2023
1 parent 351a5ae commit cafad89
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 46 deletions.
103 changes: 68 additions & 35 deletions netjsonconfig/backends/openwrt/converters/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class Interfaces(OpenWrtConverter):
'ethernet',
'bridge',
'wireless',
'8021q',
'8021ad',
] + _proto_dsa_conflict

def __set_dsa_interface(self, interface):
Expand All @@ -61,29 +63,30 @@ def to_intermediate_loop(self, block, result, index=None):
uci_device = self.__intermediate_device(interface)
if uci_device:
result['network'].append(self.sorted_dict(uci_device))
# create one or more "config interface" UCI blocks
i = 1
for address in address_list:
uci_interface = deepcopy(interface)
# add suffix to logical name when
# there is more than one interface
if i > 1:
uci_interface['.name'] = '{name}_{i}'.format(name=uci_name, i=i)
uci_interface.update(
{
'dns': self.__intermediate_dns_servers(uci_interface, address),
'dns_search': self.__intermediate_dns_search(
uci_interface, address
),
'proto': self.__intermediate_proto(uci_interface, address),
}
)
uci_interface = self.__intermediate_bridge(uci_interface, i)
if address:
uci_interface.update(address)
result.setdefault('network', [])
result['network'].append(self.sorted_dict(uci_interface))
i += 1
if interface:
# create one or more "config interface" UCI blocks
i = 1
for address in address_list:
uci_interface = deepcopy(interface)
# add suffix to logical name when
# there is more than one interface
if i > 1:
uci_interface['.name'] = '{name}_{i}'.format(name=uci_name, i=i)
uci_interface.update(
{
'dns': self.__intermediate_dns_servers(uci_interface, address),
'dns_search': self.__intermediate_dns_search(
uci_interface, address
),
'proto': self.__intermediate_proto(uci_interface, address),
}
)
uci_interface = self.__intermediate_bridge(uci_interface, i)
if address:
uci_interface.update(address)
result.setdefault('network', [])
result['network'].append(self.sorted_dict(uci_interface))
i += 1
return result

def __intermediate_addresses(self, interface):
Expand Down Expand Up @@ -193,6 +196,17 @@ def _intermediate_vxlan(self, interface):
interface['vid'] = interface.pop('vni')
return interface

def _intermediate_8021_vlan(self, interface):
interface['name'] = '{}.{}'.format(interface['ifname'], interface['vid'])
interface['.name'] = '{}_{}'.format(interface['.name'], interface['vid'])
return interface

def _intermediate_8021q(self, interface):
return self._intermediate_8021_vlan(interface)

def _intermediate_8021ad(self, interface):
return self._intermediate_8021_vlan(interface)

_address_keys = ['address', 'mask', 'family', 'gateway']

def __intermediate_address(self, address):
Expand All @@ -210,7 +224,6 @@ def __intermediate_device(self, interface):
data structure compatible with new syntax
introduced in OpenWrt 21.02.
"""

device = {}
# Add L2 options (needed for > OpenWrt 21.02)
self._add_l2_options(device, interface)
Expand All @@ -224,8 +237,23 @@ def __intermediate_device(self, interface):
# Add 'device' option in related interface configuration
if not interface.get('device', None):
interface['device'] = device['name']

if interface['type'] != 'bridge':
interface_type = interface['type']
if interface_type.startswith('8021'):
device.update(
{
'type': interface.pop('type'),
'vid': interface.pop('vid'),
'name': interface.pop('name'),
'ifname': interface.pop('ifname'),
'ingress_qos_mapping': interface.pop('ingress_qos_mapping', []),
'egress_qos_mapping': interface.pop('egress_qos_mapping', []),
}
)
# The VLAN configuration is defined in "device".
# We don't need to add an interface.
for key in list(interface.keys()):
del interface[key]
if interface_type != 'bridge':
# A non-bridge interface that contains L2 options.
return device
device['type'] = 'bridge'
Expand Down Expand Up @@ -432,6 +460,8 @@ def __netjson_interface(self, interface):
interface['disabled'] = interface.pop('enabled') == '0'
if 'mtu' in interface:
interface['mtu'] = int(interface['mtu'])
if 'vid' in interface:
interface['vid'] = int(interface['vid'])
if 'macaddr' in interface:
interface['mac'] = interface.pop('macaddr')
if interface['network'] == self._get_uci_name(interface['name']):
Expand Down Expand Up @@ -522,15 +552,18 @@ def __netjson_device(self, interface):
self._device_config[name] = interface

def __netjson_type(self, interface):
if 'type' in interface and interface['type'] == 'bridge':
interface['bridge_members'] = interface['name'].split()
interface['name'] = 'br-{0}'.format(interface['network'])
# cleanup automatically generated "br_" network prefix
interface['name'] = interface['name'].replace('br_', '')
self.__netjson_bridge_typecast(interface)
if interface.pop('bridge_empty', None) == '1':
interface['bridge_members'] = []
return 'bridge'
if 'type' in interface:
if interface['type'] == 'bridge':
interface['bridge_members'] = interface['name'].split()
interface['name'] = 'br-{0}'.format(interface['network'])
# cleanup automatically generated "br_" network prefix
interface['name'] = interface['name'].replace('br_', '')
self.__netjson_bridge_typecast(interface)
if interface.pop('bridge_empty', None) == '1':
interface['bridge_members'] = []
return 'bridge'
if interface['type'].startswith('802'):
return interface['type']
if interface['name'] in ['lo', 'lo0', 'loopback']:
return 'loopback'
return 'ethernet'
Expand Down
23 changes: 13 additions & 10 deletions netjsonconfig/backends/openwrt/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,18 @@ def _get_uci_blocks(self, text):
# list options
else:
block[key] = block.get(key, []) + [value]
# The new bridge syntax of OpenWrt moved "bridges"
# under "device" config_type. netjsonconfig
# process bridges using the interface converter,
# therefore we need to update block type here.
if block['.type'] == 'device':
block['.type'] = 'interface'
if block.get('type') == 'bridge':
block['bridge_21'] = True
else:
block['type'] = 'device'
self._set_uci_block_type(block)
blocks.append(sorted_dict(block))
return blocks

def _set_uci_block_type(self, block):
# The new bridge syntax of OpenWrt moved "bridges"
# under "device" config_type. netjsonconfig
# process bridges using the interface converter,
# therefore we need to update block type here.
if block['.type'] == 'device':
block['.type'] = 'interface'
if block.get('type') == 'bridge':
block['bridge_21'] = True
elif not block.get('type', None):
block['type'] = 'device'
88 changes: 87 additions & 1 deletion netjsonconfig/backends/openwrt/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
from ..wireguard.schema import base_wireguard_schema
from .timezones import timezones

QOS_MAPPING_PATTERN = "^[0-9]\d*:[0-9]\d*$"

default_radio_driver = "mac80211"

wireguard = base_wireguard_schema["properties"]["wireguard"]["items"]["properties"]
wireguard_peers = wireguard["peers"]["items"]["properties"]
interface_settings = default_schema["definitions"]["interface_settings"]["properties"]


schema = merge_config(
default_schema,
{
Expand All @@ -30,6 +31,49 @@
}
}
},
"vlan_interface_settings": {
"properties": {
"name": {"title": "Base device"},
"vid": {
"type": "integer",
"title": "VLAN ID",
"propertyOrder": 2,
"minimum": 0,
},
"ingress_qos_mapping": {
"type": "array",
"title": "Ingress QoS mapping",
"description": (
"Defines a mapping of VLAN header priority to the Linux"
" internal packet priority on incoming frames"
),
"uniqueItems": True,
"additionalItems": False,
"items": {
"title": "Mapping",
"type": "string",
"pattern": QOS_MAPPING_PATTERN,
},
"propertyOrder": 18,
},
"egress_qos_mapping": {
"type": "array",
"title": "Egress QoS mapping",
"description": (
"Defines a mapping of Linux internal packet priority to VLAN header"
" priority but for outgoing frames"
),
"uniqueItems": True,
"additionalItems": False,
"items": {
"title": "Mapping",
"type": "string",
"pattern": QOS_MAPPING_PATTERN,
},
"propertyOrder": 19,
},
}
},
"wireless_interface": {
"properties": {
"wireless": {
Expand Down Expand Up @@ -268,6 +312,46 @@
}
]
},
"vlan_8021q": {
"title": "VLAN (802.1q)",
"type": "object",
"required": ["type", "vid"],
"allOf": [
{
"properties": {
"type": {
"type": "string",
"enum": ["8021q"],
"default": "8021q",
"propertyOrder": 1,
},
}
},
{"$ref": "#/definitions/base_interface_settings"},
{"$ref": "#/definitions/interface_settings"},
{"$ref": "#/definitions/vlan_interface_settings"},
],
},
"vlan_8021ad": {
"title": "VLAN (802.1ad)",
"type": "object",
"required": ["type", "vid"],
"allOf": [
{
"properties": {
"type": {
"type": "string",
"enum": ["8021ad"],
"default": "8021ad",
"propertyOrder": 1,
},
}
},
{"$ref": "#/definitions/base_interface_settings"},
{"$ref": "#/definitions/interface_settings"},
{"$ref": "#/definitions/vlan_interface_settings"},
],
},
"dialup_interface": {
"title": "Dialup interface",
"required": ["proto", "username", "password"],
Expand Down Expand Up @@ -645,6 +729,8 @@
{"$ref": "#/definitions/modemmanager_interface"},
{"$ref": "#/definitions/vxlan_interface"},
{"$ref": "#/definitions/wireguard_interface"},
{"$ref": "#/definitions/vlan_8021q"},
{"$ref": "#/definitions/vlan_8021ad"},
]
}
},
Expand Down
68 changes: 68 additions & 0 deletions tests/openwrt/test_interfaces_dsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -1907,3 +1907,71 @@ def test_empty_dns(self):
"""
)
self.assertEqual(o.render(), expected)

_vlan8021q_netjson = {
"interfaces": [
{
"type": "8021q",
"vid": 1,
"name": "br-lan",
"mac": "E8:6A:64:3E:4A:3A",
"mtu": 1500,
"ingress_qos_mapping": ["1:1"],
"egress_qos_mapping": ["2:2"],
}
]
}

_vlan8021q_uci = """package network
config device 'device_br_lan_1'
list egress_qos_mapping '2:2'
option ifname 'br-lan'
list ingress_qos_mapping '1:1'
option macaddr 'E8:6A:64:3E:4A:3A'
option mtu '1500'
option name 'br-lan.1'
option type '8021q'
option vid '1'
"""

def test_render_vlan8021q(self):
o = OpenWrt(self._vlan8021q_netjson)
expected = self._tabs(self._vlan8021q_uci)
self.assertEqual(o.render(), expected)

def test_parse_vlan8021q(self):
o = OpenWrt(native=self._tabs(self._vlan8021q_uci))
expected = deepcopy(self._vlan8021q_netjson)
expected['interfaces'][0]['network'] = 'device_br_lan_1'
self.assertEqual(expected, o.config)

_vlan8021ad_netjson = {
"interfaces": [
{
"type": "8021ad",
"vid": 6,
"name": "eth0",
}
]
}

_vlan8021ad_uci = """package network
config device 'device_eth0_6'
option ifname 'eth0'
option name 'eth0.6'
option type '8021ad'
option vid '6'
"""

def test_render_vlan8021ad(self):
o = OpenWrt(self._vlan8021ad_netjson)
expected = self._tabs(self._vlan8021ad_uci)
self.assertEqual(o.render(), expected)

def test_parse_vlan8021ad(self):
o = OpenWrt(native=self._tabs(self._vlan8021ad_uci))
expected = deepcopy(self._vlan8021ad_netjson)
expected['interfaces'][0]['network'] = 'device_eth0_6'
self.assertEqual(expected, o.config)

0 comments on commit cafad89

Please sign in to comment.