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.bridgedomain import validate_bridgedomains
|
||||
from config.vxlan_tunnel import validate_vxlan_tunnels
|
||||
from config.tap import validate_taps
|
||||
|
||||
from yamale.validators import DefaultValidators, Validator
|
||||
|
||||
@ -136,6 +137,12 @@ class Validator(object):
|
||||
if not rv:
|
||||
ret_rv = False
|
||||
|
||||
rv, msgs = validate_taps(yaml)
|
||||
if msgs:
|
||||
ret_msgs.extend(msgs)
|
||||
if not rv:
|
||||
ret_rv = False
|
||||
|
||||
if ret_rv:
|
||||
self.logger.debug("Semantics correctly validated")
|
||||
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
|
||||
```
|
||||
|
||||
### 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 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)
|
||||
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)
|
||||
taps: map(include('tap'),key=str(matches='tap[0-9]+'),required=False)
|
||||
---
|
||||
vxlan:
|
||||
description: str(exclude='\'"',len=64,required=False)
|
||||
@ -62,3 +63,16 @@ encapsulation:
|
||||
dot1ad: int(min=1,max=4095,required=False)
|
||||
inner-dot1q: int(min=1,max=4095,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