From d6e3496809c6faaf5cd56939558a726c2d47dc35 Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Sat, 9 Apr 2022 19:56:48 +0000 Subject: [PATCH] Add TAP syntax/semantic validator --- config/__init__.py | 7 +++ config/tap.py | 101 +++++++++++++++++++++++++++++++++ config/test_tap.py | 35 ++++++++++++ docs/config-guide.md | 64 +++++++++++++++++++++ schema.yaml | 14 +++++ unittest/test_tap.yaml | 22 +++++++ unittest/yaml/correct-tap.yaml | 19 +++++++ unittest/yaml/error-tap1.yaml | 19 +++++++ unittest/yaml/error-tap2.yaml | 14 +++++ unittest/yaml/error-tap3.yaml | 15 +++++ unittest/yaml/error-tap4.yaml | 23 ++++++++ unittest/yaml/error-tap5.yaml | 34 +++++++++++ 12 files changed, 367 insertions(+) create mode 100644 config/tap.py create mode 100644 config/test_tap.py create mode 100644 unittest/test_tap.yaml create mode 100644 unittest/yaml/correct-tap.yaml create mode 100644 unittest/yaml/error-tap1.yaml create mode 100644 unittest/yaml/error-tap2.yaml create mode 100644 unittest/yaml/error-tap3.yaml create mode 100644 unittest/yaml/error-tap4.yaml create mode 100644 unittest/yaml/error-tap5.yaml diff --git a/config/__init__.py b/config/__init__.py index 6ffa82c..0b4d894 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -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 diff --git a/config/tap.py b/config/tap.py new file mode 100644 index 0000000..c750c11 --- /dev/null +++ b/config/tap.py @@ -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 diff --git a/config/test_tap.py b/config/test_tap.py new file mode 100644 index 0000000..df3015d --- /dev/null +++ b/config/test_tap.py @@ -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) diff --git a/docs/config-guide.md b/docs/config-guide.md index 4fd6810..bc214f5 100644 --- a/docs/config-guide.md +++ b/docs/config-guide.md @@ -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 diff --git a/schema.yaml b/schema.yaml index ca10af5..41037b4 100644 --- a/schema.yaml +++ b/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) diff --git a/unittest/test_tap.yaml b/unittest/test_tap.yaml new file mode 100644 index 0000000..74798b2 --- /dev/null +++ b/unittest/test_tap.yaml @@ -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 diff --git a/unittest/yaml/correct-tap.yaml b/unittest/yaml/correct-tap.yaml new file mode 100644 index 0000000..2ca25e1 --- /dev/null +++ b/unittest/yaml/correct-tap.yaml @@ -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 diff --git a/unittest/yaml/error-tap1.yaml b/unittest/yaml/error-tap1.yaml new file mode 100644 index 0000000..dc7ac2a --- /dev/null +++ b/unittest/yaml/error-tap1.yaml @@ -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 diff --git a/unittest/yaml/error-tap2.yaml b/unittest/yaml/error-tap2.yaml new file mode 100644 index 0000000..1cd0562 --- /dev/null +++ b/unittest/yaml/error-tap2.yaml @@ -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 diff --git a/unittest/yaml/error-tap3.yaml b/unittest/yaml/error-tap3.yaml new file mode 100644 index 0000000..592652c --- /dev/null +++ b/unittest/yaml/error-tap3.yaml @@ -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 diff --git a/unittest/yaml/error-tap4.yaml b/unittest/yaml/error-tap4.yaml new file mode 100644 index 0000000..e08b02f --- /dev/null +++ b/unittest/yaml/error-tap4.yaml @@ -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 diff --git a/unittest/yaml/error-tap5.yaml b/unittest/yaml/error-tap5.yaml new file mode 100644 index 0000000..c283c76 --- /dev/null +++ b/unittest/yaml/error-tap5.yaml @@ -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