Add TAP syntax/semantic validator

This commit is contained in:
Pim van Pelt
2022-04-09 19:56:48 +00:00
parent 92a20d0755
commit d6e3496809
12 changed files with 367 additions and 0 deletions

View File

@ -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
View 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
View 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)

View File

@ -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

View File

@ -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
View 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

View 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

View 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

View 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

View 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

View 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

View 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