Add TAP syntax/semantic validator
This commit is contained in:
@ -33,6 +33,7 @@ from config.bondethernet import validate_bondethernets
|
|||||||
from config.interface import validate_interfaces
|
from config.interface import validate_interfaces
|
||||||
from config.bridgedomain import validate_bridgedomains
|
from config.bridgedomain import validate_bridgedomains
|
||||||
from config.vxlan_tunnel import validate_vxlan_tunnels
|
from config.vxlan_tunnel import validate_vxlan_tunnels
|
||||||
|
from config.tap import validate_taps
|
||||||
|
|
||||||
from yamale.validators import DefaultValidators, Validator
|
from yamale.validators import DefaultValidators, Validator
|
||||||
|
|
||||||
@ -136,6 +137,12 @@ class Validator(object):
|
|||||||
if not rv:
|
if not rv:
|
||||||
ret_rv = False
|
ret_rv = False
|
||||||
|
|
||||||
|
rv, msgs = validate_taps(yaml)
|
||||||
|
if msgs:
|
||||||
|
ret_msgs.extend(msgs)
|
||||||
|
if not rv:
|
||||||
|
ret_rv = False
|
||||||
|
|
||||||
if ret_rv:
|
if ret_rv:
|
||||||
self.logger.debug("Semantics correctly validated")
|
self.logger.debug("Semantics correctly validated")
|
||||||
return ret_rv, ret_msgs
|
return ret_rv, ret_msgs
|
||||||
|
101
config/tap.py
Normal file
101
config/tap.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2022 Pim van Pelt
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at:
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
import config.mac as mac
|
||||||
|
|
||||||
|
def get_taps(yaml):
|
||||||
|
""" Return a list of all taps. """
|
||||||
|
ret = []
|
||||||
|
if 'taps' in yaml:
|
||||||
|
for ifname, iface in yaml['taps'].items():
|
||||||
|
ret.append(ifname)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def get_by_name(yaml, ifname):
|
||||||
|
""" Return the tap by name, if it exists. Return None otherwise. """
|
||||||
|
try:
|
||||||
|
if ifname in yaml['taps']:
|
||||||
|
return ifname, yaml['taps'][ifname]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def is_tap(yaml, ifname):
|
||||||
|
""" Returns True if the interface name is an existing tap in the config.
|
||||||
|
The TAP has to be explicitly named in the configuration, and notably
|
||||||
|
a TAP belonging to a Linux Control Plane (LCP) will return False.
|
||||||
|
"""
|
||||||
|
ifname, iface = get_by_name(yaml, ifname)
|
||||||
|
return not iface == None
|
||||||
|
|
||||||
|
|
||||||
|
def is_host_name_unique(yaml, hostname):
|
||||||
|
""" Returns True if there is at most one occurence of the given ifname amonst all host-names of TAPs. """
|
||||||
|
if not 'taps' in yaml:
|
||||||
|
return True
|
||||||
|
host_names = []
|
||||||
|
for tap_ifname, tap_iface in yaml['taps'].items():
|
||||||
|
host_names.append(tap_iface['host']['name'])
|
||||||
|
return host_names.count(hostname) < 2
|
||||||
|
|
||||||
|
|
||||||
|
def validate_taps(yaml):
|
||||||
|
result = True
|
||||||
|
msgs = []
|
||||||
|
logger = logging.getLogger('vppcfg.config')
|
||||||
|
logger.addHandler(logging.NullHandler())
|
||||||
|
|
||||||
|
if not 'taps' in yaml:
|
||||||
|
return result, msgs
|
||||||
|
|
||||||
|
for ifname, iface in yaml['taps'].items():
|
||||||
|
logger.debug("tap %s" % iface)
|
||||||
|
instance = int(ifname[3:])
|
||||||
|
|
||||||
|
## NOTE(pim): 1024 is not off-by-one, tap1024 is precisely the highest permissible id
|
||||||
|
if instance > 1024:
|
||||||
|
msgs.append("tap %s has instance %d which is too large" % (ifname, instance))
|
||||||
|
result = False
|
||||||
|
|
||||||
|
if not is_host_name_unique(yaml, iface['host']['name']):
|
||||||
|
msgs.append("tap %s does not have a unique host name %s" % (ifname, iface['host']['name']))
|
||||||
|
result = False
|
||||||
|
|
||||||
|
if 'rx-ring-size' in iface:
|
||||||
|
n = iface['rx-ring-size']
|
||||||
|
if n & (n-1) != 0:
|
||||||
|
msgs.append("tap %s rx-ring-size must be a power of two" % (ifname))
|
||||||
|
result = False
|
||||||
|
|
||||||
|
if 'tx-ring-size' in iface:
|
||||||
|
n = iface['tx-ring-size']
|
||||||
|
if n & (n-1) != 0:
|
||||||
|
msgs.append("tap %s tx-ring-size must be a power of two" % (ifname))
|
||||||
|
result = False
|
||||||
|
|
||||||
|
if 'namespace-create' in iface['host'] and iface['host']['namespace-create'] and not 'namespace' in iface['host']:
|
||||||
|
msgs.append("tap %s namespace-create can only be set if namespace is set" % (ifname))
|
||||||
|
result = False
|
||||||
|
|
||||||
|
if 'bridge-create' in iface['host'] and iface['host']['bridge-create'] and not 'bridge' in iface['host']:
|
||||||
|
msgs.append("tap %s bridge-create can only be set if bridge is set" % (ifname))
|
||||||
|
result = False
|
||||||
|
|
||||||
|
if 'mac' in iface['host'] and mac.is_multicast(iface['host']['mac']):
|
||||||
|
msgs.append("tap %s host MAC address %s cannot be multicast" % (ifname, iface['host']['mac']))
|
||||||
|
result = False
|
||||||
|
|
||||||
|
return result, msgs
|
35
config/test_tap.py
Normal file
35
config/test_tap.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import unittest
|
||||||
|
import yaml
|
||||||
|
import config.tap as tap
|
||||||
|
|
||||||
|
class TestTAPMethods(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
with open("unittest/test_tap.yaml", "r") as f:
|
||||||
|
self.cfg = yaml.load(f, Loader = yaml.FullLoader)
|
||||||
|
|
||||||
|
def test_get_by_name(self):
|
||||||
|
ifname, iface = tap.get_by_name(self.cfg, "tap0")
|
||||||
|
self.assertIsNotNone(iface)
|
||||||
|
self.assertEqual("tap0", ifname)
|
||||||
|
|
||||||
|
ifname, iface = tap.get_by_name(self.cfg, "tap-noexist")
|
||||||
|
self.assertIsNone(ifname)
|
||||||
|
self.assertIsNone(iface)
|
||||||
|
|
||||||
|
def test_is_tap(self):
|
||||||
|
self.assertTrue(tap.is_tap(self.cfg, "tap0"))
|
||||||
|
self.assertTrue(tap.is_tap(self.cfg, "tap1"))
|
||||||
|
self.assertFalse(tap.is_tap(self.cfg, "tap-noexist"))
|
||||||
|
|
||||||
|
def test_is_host_name_unique(self):
|
||||||
|
self.assertTrue(tap.is_host_name_unique(self.cfg, "tap0"))
|
||||||
|
self.assertTrue(tap.is_host_name_unique(self.cfg, "tap1"))
|
||||||
|
self.assertTrue(tap.is_host_name_unique(self.cfg, "tap-noexist"))
|
||||||
|
self.assertFalse(tap.is_host_name_unique(self.cfg, "vpp-tap"))
|
||||||
|
|
||||||
|
def test_enumerators(self):
|
||||||
|
ifs = tap.get_taps(self.cfg)
|
||||||
|
self.assertEqual(len(ifs), 4)
|
||||||
|
self.assertIn("tap0", ifs)
|
||||||
|
self.assertIn("tap1", ifs)
|
||||||
|
self.assertNotIn("tap-noexist", ifs)
|
@ -213,6 +213,70 @@ vxlan_tunnels:
|
|||||||
vni: 101
|
vni: 101
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### TAPs
|
||||||
|
|
||||||
|
TAPs are virtual L2 (and sometimes L3) devices in the kernel, that are backed by a userspace
|
||||||
|
program. VPP can create a TAP and expose them in a network namespace, and optionally add them
|
||||||
|
to a (Linux) bridge.
|
||||||
|
|
||||||
|
TAPs are required to be named `tapN` where N in [0,1024], but be aware that Linux CP will use TAPs
|
||||||
|
with an instance id that equals their hardware interface id. It is safer to create TAPs from the top
|
||||||
|
of the namespace, for example `tap100`, see the caveat below on why. The configuration then allows
|
||||||
|
for the following fields:
|
||||||
|
|
||||||
|
* ***description***: A string, no longer than 64 characters, and excluding the single quote '
|
||||||
|
and double quote ". This string is currently not used anywhere, and serves for enduser
|
||||||
|
documentation purposes.
|
||||||
|
* ***host***: Configuration of the Linux side of the TAP:
|
||||||
|
* ***name***: A (mandatory) Linux interface name, at most 15 characters long, matching the
|
||||||
|
regular expression `[a-z]+[a-z0-9-]*`.
|
||||||
|
* ***mac***: The MAC address for the Linux interface, if empty it will be randomly assigned.
|
||||||
|
* ***mtu***: The MTU of the Linux interface, if empty it will be set to 1500.
|
||||||
|
* ***bridge***: An optional Linux bridge to add the Linux interface into. Note: VPP will
|
||||||
|
expect this bridge to exist, otherwise the addition will silently fail after creating the TAP.
|
||||||
|
* ***namespace***: An optional Linux network namespace in which to add the Linux interface,
|
||||||
|
which can be empty (the default) in which case the Linux interface is created in the default
|
||||||
|
namespace.
|
||||||
|
* ***bridge-create***: A boolean that determines if vppcfg will create the bridge in the namespace
|
||||||
|
if it does not yet exist, and will set its MTU to the `host.mtu` value if it does exist.
|
||||||
|
Defaults to False, and can only be True if `bridge` is given.
|
||||||
|
* ***namespace-create***: A boolean that determines if vppcfg will create the network namespace
|
||||||
|
if it does not yet exist. Defaults to False, and can only be True if `namespace` is given.
|
||||||
|
* ***rx-ring-size***: An optional RX ringbuffer size, a value from 8 to 32K, must be a power of two.
|
||||||
|
If it is not specified, it will default to 256.
|
||||||
|
* ***tx-ring-size***: An optional TX ringbuffer size, a value from 8 to 32K, must be a power of two.
|
||||||
|
If it is not specified, it will default to 256.
|
||||||
|
|
||||||
|
*NOTE*: The Linux Controlplane (LCP) plugin in VPP also uses TAPs to expose the dataplane (sub-)
|
||||||
|
interfaces in Linux, but for that functionality, refer to the `lcp` fields in interfaces and loopbacks.
|
||||||
|
|
||||||
|
*Caveat*: syncing changed attributes (with the exception of the bridge name) after the TAP was created
|
||||||
|
is not supported. This is because there are no API setters in VPP. Changing attributes is possible, but
|
||||||
|
operators should expect that the TAP interface gets pruned and recreated.
|
||||||
|
|
||||||
|
*Caveat*: `vppcfg` will try to ensure a TAP is not created with the same instance ID as a hardware
|
||||||
|
interface, but it can not make strict guarantees, because there exists no API to look the hardware
|
||||||
|
interface id's up. As a rule of thumb, start TAPs at twice the total count of hardware interfaces
|
||||||
|
(PHYs, BondEthernets, VXLAN Tunnels and other TAPs) in the config.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```
|
||||||
|
taps:
|
||||||
|
tap100:
|
||||||
|
description: "TAP with MAC, MTU and Bridge"
|
||||||
|
host:
|
||||||
|
name: vpp-tap100
|
||||||
|
mac: f6:18:fe:e7:d2:3a
|
||||||
|
mtu: 9000
|
||||||
|
namespace: test
|
||||||
|
namespace-create: True
|
||||||
|
bridge: vpp-br0
|
||||||
|
bridge-create: True
|
||||||
|
rx-ring-size: 1024
|
||||||
|
tx-ring-size: 512
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Interfaces
|
### Interfaces
|
||||||
|
|
||||||
Interfaces and their sub-interfaces are configured very similarly. Interface names MUST either
|
Interfaces and their sub-interfaces are configured very similarly. Interface names MUST either
|
||||||
|
14
schema.yaml
14
schema.yaml
@ -3,6 +3,7 @@ bondethernets: map(include('bondethernet'),key=str(matches='BondEthernet[0-9]+')
|
|||||||
loopbacks: map(include('loopback'),key=str(matches='loop[0-9]+'),required=False)
|
loopbacks: map(include('loopback'),key=str(matches='loop[0-9]+'),required=False)
|
||||||
bridgedomains: map(include('bridgedomain'),key=str(matches='bd[0-9]+'),required=False)
|
bridgedomains: map(include('bridgedomain'),key=str(matches='bd[0-9]+'),required=False)
|
||||||
vxlan_tunnels: map(include('vxlan'),key=str(matches='vxlan_tunnel[0-9]+'),required=False)
|
vxlan_tunnels: map(include('vxlan'),key=str(matches='vxlan_tunnel[0-9]+'),required=False)
|
||||||
|
taps: map(include('tap'),key=str(matches='tap[0-9]+'),required=False)
|
||||||
---
|
---
|
||||||
vxlan:
|
vxlan:
|
||||||
description: str(exclude='\'"',len=64,required=False)
|
description: str(exclude='\'"',len=64,required=False)
|
||||||
@ -62,3 +63,16 @@ encapsulation:
|
|||||||
dot1ad: int(min=1,max=4095,required=False)
|
dot1ad: int(min=1,max=4095,required=False)
|
||||||
inner-dot1q: int(min=1,max=4095,required=False)
|
inner-dot1q: int(min=1,max=4095,required=False)
|
||||||
exact-match: bool(required=False)
|
exact-match: bool(required=False)
|
||||||
|
---
|
||||||
|
tap:
|
||||||
|
description: str(exclude='\'"',len=64,required=False)
|
||||||
|
host:
|
||||||
|
name: str(max=15,matches='[a-z]+[a-z0-9-]*')
|
||||||
|
mac: mac(required=False)
|
||||||
|
mtu: int(min=128,max=9216,required=False)
|
||||||
|
bridge: str(max=15,matches='[a-z]+[a-z0-9-]*',required=False)
|
||||||
|
bridge-create: bool(required=False)
|
||||||
|
namespace: str(max=64,matches='[a-z]+[a-z0-9-]*',required=False)
|
||||||
|
namespace-create: bool(required=False)
|
||||||
|
rx-ring-size: int(min=8,max=32768,required=False)
|
||||||
|
tx-ring-size: int(min=8,max=32768,required=False)
|
||||||
|
22
unittest/test_tap.yaml
Normal file
22
unittest/test_tap.yaml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
taps:
|
||||||
|
tap0:
|
||||||
|
description: "TAP with MAC, MTU and Bridge"
|
||||||
|
mac: 00:01:02:03:04:05
|
||||||
|
host:
|
||||||
|
mtu: 9216
|
||||||
|
name: vpp-tap0
|
||||||
|
bridge: br0
|
||||||
|
rx-ring-size: 256
|
||||||
|
tx-ring-size: 256
|
||||||
|
tap1:
|
||||||
|
description: "TAP, no config other than mandatory host-name"
|
||||||
|
host:
|
||||||
|
name: vpp-tap1
|
||||||
|
tap2:
|
||||||
|
description: "TAP, which has an overlapping host-name"
|
||||||
|
host:
|
||||||
|
name: vpp-tap
|
||||||
|
tap3:
|
||||||
|
description: "TAP, which has an overlapping host-name"
|
||||||
|
host:
|
||||||
|
name: vpp-tap
|
19
unittest/yaml/correct-tap.yaml
Normal file
19
unittest/yaml/correct-tap.yaml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
test:
|
||||||
|
description: "A few correct examples of well formed TAP interfaces"
|
||||||
|
errors:
|
||||||
|
count: 0
|
||||||
|
---
|
||||||
|
taps:
|
||||||
|
tap0:
|
||||||
|
description: "TAP with MAC, MTU and Bridge"
|
||||||
|
host:
|
||||||
|
name: vpp-tap0
|
||||||
|
mac: 00:01:02:03:04:05
|
||||||
|
mtu: 9216
|
||||||
|
bridge: br0
|
||||||
|
rx-ring-size: 256
|
||||||
|
tx-ring-size: 256
|
||||||
|
tap1:
|
||||||
|
description: "TAP, no config other than mandatory host-name"
|
||||||
|
host:
|
||||||
|
name: vpp-tap1
|
19
unittest/yaml/error-tap1.yaml
Normal file
19
unittest/yaml/error-tap1.yaml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
test:
|
||||||
|
description: "Instance id must be between [0..1024]"
|
||||||
|
errors:
|
||||||
|
expected:
|
||||||
|
- "tap .* has instance .* which is too large"
|
||||||
|
count: 1
|
||||||
|
---
|
||||||
|
taps:
|
||||||
|
tap0:
|
||||||
|
host:
|
||||||
|
name: vpp-tap0
|
||||||
|
tap1024:
|
||||||
|
description: "Cool"
|
||||||
|
host:
|
||||||
|
name: vpp-tap1024
|
||||||
|
tap1025:
|
||||||
|
description: "Not cool"
|
||||||
|
host:
|
||||||
|
name: vpp-tap1025
|
14
unittest/yaml/error-tap2.yaml
Normal file
14
unittest/yaml/error-tap2.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
test:
|
||||||
|
description: "RX and TX ring sizes must be power of two, at most 32K"
|
||||||
|
errors:
|
||||||
|
expected:
|
||||||
|
- "tap .* rx-ring-size must be a power of two"
|
||||||
|
- "tap .* tx-ring-size must be a power of two"
|
||||||
|
count: 2
|
||||||
|
---
|
||||||
|
taps:
|
||||||
|
tap0:
|
||||||
|
host:
|
||||||
|
name: vpp-tap0
|
||||||
|
rx-ring-size: 1023
|
||||||
|
tx-ring-size: 32767
|
15
unittest/yaml/error-tap3.yaml
Normal file
15
unittest/yaml/error-tap3.yaml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
test:
|
||||||
|
description: "TAP host names must be unique"
|
||||||
|
errors:
|
||||||
|
expected:
|
||||||
|
- "tap .* does not have a unique host name .*"
|
||||||
|
count: 2
|
||||||
|
---
|
||||||
|
taps:
|
||||||
|
tap0:
|
||||||
|
host:
|
||||||
|
name: vpp-tap
|
||||||
|
|
||||||
|
tap1:
|
||||||
|
host:
|
||||||
|
name: vpp-tap
|
23
unittest/yaml/error-tap4.yaml
Normal file
23
unittest/yaml/error-tap4.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
test:
|
||||||
|
description: "TAP host mac addresses cannot be multicast"
|
||||||
|
errors:
|
||||||
|
expected:
|
||||||
|
- "tap .* host MAC address .* cannot be multicast"
|
||||||
|
count: 1
|
||||||
|
---
|
||||||
|
taps:
|
||||||
|
tap0:
|
||||||
|
description: "Cool, local MACs are fine"
|
||||||
|
host:
|
||||||
|
mac: 02:00:00:00:00:00
|
||||||
|
name: vpp-tap0
|
||||||
|
tap1:
|
||||||
|
description: "Cool, global unicast MACs are fine"
|
||||||
|
host:
|
||||||
|
mac: 04:00:00:00:00:00
|
||||||
|
name: vpp-tap1
|
||||||
|
tap2:
|
||||||
|
description: "Not cool, multicast MACs"
|
||||||
|
host:
|
||||||
|
mac: 01:00:00:00:00:00
|
||||||
|
name: vpp-tap2
|
34
unittest/yaml/error-tap5.yaml
Normal file
34
unittest/yaml/error-tap5.yaml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
test:
|
||||||
|
description: "Creating bridge or namespace can only be asked if their name is specified"
|
||||||
|
errors:
|
||||||
|
expected:
|
||||||
|
- "tap .* bridge-create can only be set if bridge is set"
|
||||||
|
- "tap .* namespace-create can only be set if namespace is set"
|
||||||
|
count: 2
|
||||||
|
---
|
||||||
|
taps:
|
||||||
|
tap0:
|
||||||
|
description: "Cool, create bridge and namespace"
|
||||||
|
host:
|
||||||
|
mac: 02:00:00:00:00:00
|
||||||
|
name: vpp-tap0
|
||||||
|
bridge: vpp-br0
|
||||||
|
bridge-create: True
|
||||||
|
namespace: vpp-test
|
||||||
|
namespace-create: True
|
||||||
|
tap1:
|
||||||
|
description: "Cool, assuming the operator has created the bridge and namespace beforehand"
|
||||||
|
host:
|
||||||
|
name: vpp-tap1
|
||||||
|
bridge: vpp-br1
|
||||||
|
namespace: vpp-test
|
||||||
|
tap2:
|
||||||
|
description: "Not cool, asking to create a bridge without giving its name"
|
||||||
|
host:
|
||||||
|
name: vpp-tap2
|
||||||
|
bridge-create: True
|
||||||
|
tap3:
|
||||||
|
description: "Not cool, asking to create a namespace without giving its name"
|
||||||
|
host:
|
||||||
|
name: vpp-tap3
|
||||||
|
namespace-create: True
|
Reference in New Issue
Block a user