diff --git a/.pylintrc b/.pylintrc index 646c5ae..67808cd 100644 --- a/.pylintrc +++ b/.pylintrc @@ -14,7 +14,7 @@ ignore-patterns=test_.* # W1203 = Use lazy % formatting in logging functions # Disable the message(s) with the given id(s). -disable=R0201,W0212,W0613,C0301,R0914,C0111,W1203 +disable=R0201,W0212,W0613,C0301,R0914,W1203 [FORMAT] max-line-length=148 diff --git a/config/__init__.py b/config/__init__.py index 1e4365b..360fc43 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. # # -*- coding: utf-8 -*- +""" A vppcfg configuration module that exposes its semantic/syntax validators """ from __future__ import ( absolute_import, division, @@ -66,6 +67,15 @@ class IPInterfaceWithPrefixLength(Validator): class Validator: + """The Validator class takes a schema filename (which may be None, in which + case a built-in default is used), and a given YAML file represented as a string, + and holds it against syntax and semantic validators, returning a tuple of (bool,list) + where the boolean signals success/failure, and the list of strings are messages + that were added when validating the YAML config. + + The purpose is to ensure that the YAML file is both syntactically correct, + which is ensured by Yamale, and semantically correct, which is ensured by a set + of built-in validators, and user-added validators (see the add_validator() method).""" def __init__(self, schema): self.logger = logging.getLogger("vppcfg.config") self.logger.addHandler(logging.NullHandler()) @@ -81,6 +91,8 @@ class Validator: ] def validate(self, yaml): + """Validate the semantics of all YAML maps, by calling self.validators in turn, + and then optionally calling validators that were added with add_validator()""" ret_retval = True ret_msgs = [] if not yaml: diff --git a/config/address.py b/config/address.py index 46776d5..2afa3d9 100644 --- a/config/address.py +++ b/config/address.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +""" A vppcfg configuration module that handles addresses """ import ipaddress diff --git a/config/bondethernet.py b/config/bondethernet.py index 26bc79b..6ef17df 100644 --- a/config/bondethernet.py +++ b/config/bondethernet.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +""" A vppcfg configuration module that handles bondethernets """ import logging from config import interface from config import mac @@ -162,6 +163,7 @@ def int_to_lb(loadbalance): def validate_bondethernets(yaml): + """Validate the semantics of all YAML 'bondethernets' entries""" result = True msgs = [] logger = logging.getLogger("vppcfg.config") diff --git a/config/bridgedomain.py b/config/bridgedomain.py index bed5c5c..fd0b634 100644 --- a/config/bridgedomain.py +++ b/config/bridgedomain.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +""" A vppcfg configuration module that handles bridgedomains """ import logging from config import interface from config import loopback @@ -81,6 +82,8 @@ def bvi_unique(yaml, bviname): def get_settings(yaml, ifname): + """Return a dictionary of 'settings' including their VPP defaults, for the + bridgedomain identified by 'ifname' (bd10)""" ifname, iface = get_by_name(yaml, ifname) if not iface: return None @@ -115,6 +118,7 @@ def get_settings(yaml, ifname): def validate_bridgedomains(yaml): + """Validate the semantics of all YAML 'bridgedomains' entries""" result = True msgs = [] logger = logging.getLogger("vppcfg.config") diff --git a/config/interface.py b/config/interface.py index 6ecdad9..6ff671a 100644 --- a/config/interface.py +++ b/config/interface.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +""" A vppcfg configuration module that validates interfaces """ import logging from config import bondethernet from config import bridgedomain @@ -415,6 +416,7 @@ def get_admin_state(yaml, ifname): def validate_interfaces(yaml): + """Validate the semantics of all YAML 'interfaces' entries""" result = True msgs = [] logger = logging.getLogger("vppcfg.config") diff --git a/config/lcp.py b/config/lcp.py index 57d76f3..13a8c35 100644 --- a/config/lcp.py +++ b/config/lcp.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +""" A vppcfg configuration module that validates Linux Control Plane (lcp) elements """ def get_lcps(yaml, interfaces=True, loopbacks=True, bridgedomains=True): diff --git a/config/loopback.py b/config/loopback.py index 92b981c..3bb1d33 100644 --- a/config/loopback.py +++ b/config/loopback.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +""" A vppcfg configuration module that validates loopbacks """ import logging from config import lcp from config import address @@ -53,6 +54,7 @@ def is_loopback(yaml, ifname): def validate_loopbacks(yaml): + """Validate the semantics of all YAML 'loopbacks' entries""" result = True msgs = [] logger = logging.getLogger("vppcfg.config") diff --git a/config/mac.py b/config/mac.py index 89135b9..1f36e83 100644 --- a/config/mac.py +++ b/config/mac.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +""" A vppcfg configuration module that validates MAC addresses """ import netaddr diff --git a/config/tap.py b/config/tap.py index 9e40b30..ee89dcf 100644 --- a/config/tap.py +++ b/config/tap.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +""" A vppcfg configuration module that validates taps """ import logging from config import mac @@ -54,6 +55,7 @@ def is_host_name_unique(yaml, hostname): def validate_taps(yaml): + """Validate the semantics of all YAML 'taps' entries""" result = True msgs = [] logger = logging.getLogger("vppcfg.config") diff --git a/config/test_bondethernet.py b/config/test_bondethernet.py index 5faebea..cbc5e0b 100644 --- a/config/test_bondethernet.py +++ b/config/test_bondethernet.py @@ -1,3 +1,18 @@ +# +# 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. +# +# -*- coding: utf-8 -*- +""" Unit tests for bondethernet """ import unittest import yaml import config.bondethernet as bondethernet diff --git a/config/test_bridgedomain.py b/config/test_bridgedomain.py index f9be7ad..e415490 100644 --- a/config/test_bridgedomain.py +++ b/config/test_bridgedomain.py @@ -1,3 +1,18 @@ +# +# 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. +# +# -*- coding: utf-8 -*- +""" Unit tests for bridgedomains """ import unittest import yaml import config.bridgedomain as bridgedomain diff --git a/config/test_interface.py b/config/test_interface.py index 8c61a48..210043b 100644 --- a/config/test_interface.py +++ b/config/test_interface.py @@ -1,3 +1,18 @@ +# +# 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. +# +# -*- coding: utf-8 -*- +""" Unit tests for interfaces """ import unittest import yaml import config.interface as interface diff --git a/config/test_lcp.py b/config/test_lcp.py index 6721ed2..3f3a1c4 100644 --- a/config/test_lcp.py +++ b/config/test_lcp.py @@ -1,3 +1,18 @@ +# +# 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. +# +# -*- coding: utf-8 -*- +""" Unit tests for LCPs """ import unittest import yaml import config.lcp as lcp diff --git a/config/test_loopback.py b/config/test_loopback.py index 8d27d00..591a08c 100644 --- a/config/test_loopback.py +++ b/config/test_loopback.py @@ -1,3 +1,18 @@ +# +# 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. +# +# -*- coding: utf-8 -*- +""" Unit tests for loopbacks """ import unittest import yaml import config.loopback as loopback diff --git a/config/test_mac.py b/config/test_mac.py index 89773dd..2a079be 100644 --- a/config/test_mac.py +++ b/config/test_mac.py @@ -1,3 +1,18 @@ +# +# 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. +# +# -*- coding: utf-8 -*- +""" Unit tests for MAC addresses """ import unittest import config.mac as mac diff --git a/config/test_tap.py b/config/test_tap.py index 5af3e4f..cc804e5 100644 --- a/config/test_tap.py +++ b/config/test_tap.py @@ -1,3 +1,18 @@ +# +# 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. +# +# -*- coding: utf-8 -*- +""" Unit tests for taps """ import unittest import yaml import config.tap as tap diff --git a/config/test_vxlan_tunnel.py b/config/test_vxlan_tunnel.py index 2a1e7de..9f5c4aa 100644 --- a/config/test_vxlan_tunnel.py +++ b/config/test_vxlan_tunnel.py @@ -1,3 +1,18 @@ +# +# 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. +# +# -*- coding: utf-8 -*- +""" Unit tests for vxlan_tunnels """ import unittest import yaml import config.vxlan_tunnel as vxlan_tunnel diff --git a/config/vxlan_tunnel.py b/config/vxlan_tunnel.py index 0811cce..9c7202d 100644 --- a/config/vxlan_tunnel.py +++ b/config/vxlan_tunnel.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +""" A vppcfg configuration module that validates vxlan_tunnels """ import logging import ipaddress @@ -56,6 +57,7 @@ def get_vxlan_tunnels(yaml): def validate_vxlan_tunnels(yaml): + """Validate the semantics of all YAML 'vxlan_tunnels' entries""" result = True msgs = [] logger = logging.getLogger("vppcfg.config") diff --git a/tests.py b/tests.py index de8cecf..0bf4118 100755 --- a/tests.py +++ b/tests.py @@ -13,6 +13,7 @@ # limitations under the License. # # -*- coding: utf-8 -*- +""" This is a unit test suite for vppcfg """ import sys import glob @@ -36,6 +37,8 @@ def example_validator(_yaml): class YAMLTest(unittest.TestCase): + """This test suite takes a YAML configuration file and holds it against the syntax + (Yamale) and semantic validators, returning errors in case of validation failures.""" def __init__(self, testName, yaml_filename, yaml_schema): # calling the super class init varies for different python versions. This works for 2.7 super().__init__(testName) @@ -43,6 +46,7 @@ class YAMLTest(unittest.TestCase): self.yaml_schema = yaml_schema def test_yaml(self): + """ The test executor """ test = None cfg = None ncount = 0 @@ -103,6 +107,7 @@ class YAMLTest(unittest.TestCase): if __name__ == "__main__": + """ The main program """ parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) parser.add_argument( "-t", diff --git a/vpp/applier.py b/vpp/applier.py index d451fea..2483f76 100644 --- a/vpp/applier.py +++ b/vpp/applier.py @@ -1,3 +1,17 @@ +# +# 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. +# +# -*- coding: utf-8 -*- """ The functions in this file interact with the VPP API to modify certain interface metadata. @@ -18,31 +32,45 @@ class Applier(VPPApi): self.logger.info("VPP Applier: changing the dataplane is enabled") def set_interface_ip_address(self, ifname, address, is_set=True): + """Add (if_set=True) or remove (if_set=False) an IPv4 or IPv6 address including + prefixlen (ie 192.0.2.0/24 or 2001:db8::1/64) to an interface given by name + (ie GigabitEthernet3/0/0)""" pass def delete_loopback(self, ifname): + """Delete a loopback identified by name (ie loop0)""" pass def delete_subinterface(self, ifname): + """Delete a sub-int identified by name (ie GigabitEthernet3/0/0.100)""" pass - def set_interface_l2_tag_rewrite(self, ifname, mode): + def set_interface_l2_tag_rewrite(self, ifname, vtr_op, vtr_push_dot1q, vtr_tag1, vtr_tag2): + """Set l2 tag rewrite on an interface identified by name (ie GigabitEthernet3/0/0.100) + into a certain operational mode. TODO(pim) clarify the vtr_* arguments.""" ## somewhere in interface.api see vtr_* fields pass def set_interface_l3(self, ifname): + """Set an interface or sub-interface identified by name (ie GigabitEthernet3/0/0) + to L3 mode, removing it from bridges and l2xcs""" pass - def delete_bridgedomain(self, instance): + def delete_bridgedomain(self, bd_id): + """Delete a bridgedomain given by instance bd_id (ie 100). Cannot delete instance==0.""" pass def delete_tap(self, ifname): + """Delete a tap identified by name (ie tap100)""" pass def bond_remove_member(self, bondname, membername): + """Remove a member interface given by name (ie GigabitEthernet3/0/0) from a bondethernet + interface given by name (ie BondEthernet0)""" pass def delete_bond(self, ifname): + """Delete a bondethernet identified by name (ie BondEthernet0)""" pass def create_vxlan_tunnel(self, instance, config, is_create=True): @@ -50,15 +78,22 @@ class Applier(VPPApi): pass def set_interface_link_mtu(self, ifname, link_mtu): + """Set the max frame size of an interface given by name to the link_mtu value (typically + 1500, 9000, 9216""" + pass def lcp_delete(self, lcpname): + """Delete a linux control plane interface pair by name (ie 'xe0' or 'be10')""" pass def set_interface_packet_mtu(self, ifname, packet_mtu): + """Set the L3 MTU of an interface given by name (ie GigabitEthernet3/0/0)""" pass def set_interface_state(self, ifname, state): + """Set the admin link state (True is up, False is down) of an interface given + by name (ie GigabitEthernet3/0/0)""" pass def create_loopback_interface(self, instance, config): @@ -77,28 +112,43 @@ class Applier(VPPApi): """'config' is the YAML configuration for the taps: entry""" pass - def create_bridgedomain(self, instance, config): + def create_bridgedomain(self, bd_id, config): """'config' is the YAML configuration for the bridgedomains: entry""" pass def lcp_create(self, ifname, host_if_name): + """Create a linux control plane interface pair for an interface given by name + (ie GigabitEthernet3/0/0) under a Linux TAP device name host_if_name (ie e3-0-0)""" pass def set_interface_mac(self, ifname, mac): + """Set the MAC address of interface given by name (ie GigabitEthernet3/0/0), the + MAC is of form aa:bb:cc:dd:ee:ff""" pass def bond_add_member(self, bondname, membername): + """Add a member interface given by name (ie GigabitEthernet3/0/0) to a bondethernet + given by name (ie BondEthernet0)""" pass - def sync_bridgedomain(self, instance, config): + def sync_bridgedomain(self, bd_id, config): """'config' is the YAML configuration for the bridgedomains: entry""" pass - def set_interface_l2_bridge_bvi(self, instance, ifname): + def set_interface_l2_bridge_bvi(self, bd_id, ifname): + """Set a loopback / BVI interface given by name (ie 'loop100') as a BVI of a bridge + domain identified by bd_id (ie 100)""" pass - def set_interface_l2_bridge(self, instance, ifname): + def set_interface_l2_bridge(self, bd_id, ifname): + """Set an interface given by name (ie 'GigabitEthernet3/0/0') into a bridge + domain identified by bd_id (ie 100)""" pass def set_interface_l2xc(self, rx_ifname, tx_ifname): + """Cross connect the rx_ifname (ie GigabitEthernet3/0/0) to emit into the tx_ifname + (ie GigabitEthernet3/0/1). Note that this operation typically happens twice, once + for the a->b crossconnect, and again for the b->a crossconnect. Note that + crossconnecting sub-interfaces requires as well L2 rewriting (pop N for the amount + of tags on the source interface)""" pass diff --git a/vpp/dumper.py b/vpp/dumper.py index 77a247b..1bd3809 100644 --- a/vpp/dumper.py +++ b/vpp/dumper.py @@ -1,3 +1,17 @@ +# +# 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. +# +# -*- coding: utf-8 -*- """ The functions in this file interact with the VPP API to retrieve certain interface metadata and write it to a YAML file. @@ -10,10 +24,17 @@ from config import bondethernet class Dumper(VPPApi): + """The Dumper class first reads the configuration from a running VPP Dataplane + by using a set of (readonly) API getters, and then emits the configuration as + a YAML file with its write() method, either to stdout or to a filename. + + Note that not all running VPP configs are "valid" in vppcfg's eyes. It is not + guaranteed that the output of the Dumper() will stand validation.""" def __init__(self, address="/run/vpp/api.sock", clientname="vppcfg"): VPPApi.__init__(self, address, clientname) def write(self, outfile): + """ Emit the configuration to either stdout (outfile=='-') or a filename """ if outfile and outfile == "-": file = sys.stdout outfile = "(stdout)" @@ -29,6 +50,8 @@ class Dumper(VPPApi): self.logger.info(f"Wrote YAML config to {outfile}") def cache_to_config(self): + """ Convert the VPP configuration cache (previously read by readconfig() into + a YAML representation.""" config = { "loopbacks": {}, "bondethernets": {}, diff --git a/vpp/reconciler.py b/vpp/reconciler.py index 3400220..8eda3e8 100644 --- a/vpp/reconciler.py +++ b/vpp/reconciler.py @@ -13,6 +13,10 @@ # limitations under the License. # # -*- coding: utf-8 -*- +""" +The functions in this file interact with the VPP API to retrieve certain +metadata, and plan configuration changes towards a given YAML target configuration. +""" import sys import logging from config import loopback @@ -26,6 +30,14 @@ from vpp.vppapi import VPPApi class Reconciler: + """The Reconciler class first reads the running configuration of a VPP Dataplane, + and based on an intended target YAML configuration file, plans a path to make the + dataplane safely reflect the target config. It first prunes (removes) objects that + are not meant to be in the dataplane, or are in the dataplane but are not of the + correct create-time attributes; then it creates objects that are in the configuration + but not yet in the dataplane; and finally it syncs the configuration attributes of + objects that can be changed at runtime.""" + def __init__(self, cfg): self.logger = logging.getLogger("vppcfg.reconciler") self.logger.addHandler(logging.NullHandler()) @@ -68,12 +80,6 @@ class Reconciler: ret = False return ret - def vpp_readconfig(self): - if not self.vpp.readconfig(): - self.logger.error("Could not (re)read config from VPP") - return False - return True - def prune(self): """Remove all objects from VPP that do not occur in the config. For an indepth explanation of how and why this particular pruning order is chosen, see README.md section on @@ -748,6 +754,7 @@ class Reconciler: return ret def create_loopbacks(self): + """Create all loopbacks that occur in the config but not in VPP""" for ifname in loopback.get_loopbacks(self.cfg): if ifname in self.vpp.cache["interface_names"]: continue @@ -760,6 +767,7 @@ class Reconciler: return True def create_bondethernets(self): + """Create all bondethernets that occur in the config but not in VPP""" for ifname in bondethernet.get_bondethernets(self.cfg): if ifname in self.vpp.cache["interface_names"]: continue @@ -776,6 +784,7 @@ class Reconciler: return True def create_vxlan_tunnels(self): + """Create all vxlan_tunnels that occur in the config but not in VPP""" for ifname in vxlan_tunnel.get_vxlan_tunnels(self.cfg): if ifname in self.vpp.cache["interface_names"]: continue @@ -789,6 +798,7 @@ class Reconciler: return True def create_sub_interfaces(self): + """Create all sub-interfaces that occur in the config but not in VPP""" ## First create 1-tag (Dot1Q/Dot1AD), and then create 2-tag (Qin*) sub-interfaces for do_qinx in [False, True]: for ifname in interface.get_sub_interfaces(self.cfg): @@ -815,6 +825,7 @@ class Reconciler: return True def create_taps(self): + """Create all taps that occur in the config but not in VPP""" for ifname in tap.get_taps(self.cfg): ifname, iface = tap.get_by_name(self.cfg, ifname) if ifname in self.vpp.cache["interface_names"]: @@ -838,6 +849,7 @@ class Reconciler: return True def create_bridgedomains(self): + """Create all bridgedomains that occur in the config but not in VPP""" for ifname in bridgedomain.get_bridgedomains(self.cfg): ifname, _iface = bridgedomain.get_by_name(self.cfg, ifname) instance = int(ifname[2:]) @@ -863,6 +875,7 @@ class Reconciler: return True def create_lcps(self): + """Create all LCPs that occur in the config but not in VPP""" lcpnames = [ self.vpp.cache["lcps"][x].host_if_name for x in self.vpp.cache["lcps"] ] @@ -900,6 +913,7 @@ class Reconciler: return True def sync(self): + """Synchronize the VPP Dataplane configuration for all objects in the config""" ret = True if not self.sync_loopbacks(): self.logger.warning("Could not sync Loopbacks in VPP") @@ -928,6 +942,7 @@ class Reconciler: return ret def sync_loopbacks(self): + """Synchronize the VPP Dataplane configuration for loopbacks""" for ifname in loopback.get_loopbacks(self.cfg): if not ifname in self.vpp.cache["interface_names"]: ## New loopback @@ -942,6 +957,7 @@ class Reconciler: return True def sync_phys(self): + """Synchronize the VPP Dataplane configuration for PHYs""" for ifname in interface.get_phys(self.cfg): if not ifname in self.vpp.cache["interface_names"]: ## New interface @@ -956,6 +972,7 @@ class Reconciler: return True def sync_bondethernets(self): + """Synchronize the VPP Dataplane configuration for bondethernets""" for ifname in bondethernet.get_bondethernets(self.cfg): if ifname in self.vpp.cache["interface_names"]: vpp_iface = self.vpp.cache["interface_names"][ifname] @@ -1008,6 +1025,7 @@ class Reconciler: return True def sync_bridgedomains(self): + """Synchronize the VPP Dataplane configuration for bridgedomains""" for ifname in bridgedomain.get_bridgedomains(self.cfg): instance = int(ifname[2:]) if instance in self.vpp.cache["bridgedomains"]: @@ -1097,6 +1115,7 @@ class Reconciler: return True def sync_l2xcs(self): + """Synchronize the VPP Dataplane configuration for L2 cross connects""" for ifname in interface.get_l2xc_interfaces(self.cfg): config_rx_ifname, config_rx_iface = interface.get_by_name(self.cfg, ifname) config_tx_ifname, _config_tx_iface = interface.get_by_name( @@ -1134,6 +1153,9 @@ class Reconciler: return True def sync_mtu_direction(self, shrink=True): + """Synchronize the VPP Dataplane packet MTU, where 'shrink' determines the + direction (if shrink is True, go from inner-most (QinQ) to outer-most (untagged), + and the other direction if shrink is False""" if shrink: tag_list = [2, 1, 0] else: @@ -1176,6 +1198,9 @@ class Reconciler: return True def sync_link_mtu_direction(self, shrink=True): + """Synchronize the VPP Dataplane max frame size (link MTU), where 'shrink' determines the + direction (if shrink is True, go from inner-most (QinQ) to outer-most (untagged), + and the other direction if shrink is False""" for _idx, vpp_iface in self.vpp.cache["interfaces"].items(): if vpp_iface.sub_number_of_tags != 0: continue @@ -1231,6 +1256,7 @@ class Reconciler: return True def sync_mtu(self): + """Synchronize the VPP Dataplane configuration for interface MTU""" ret = True if not self.sync_link_mtu_direction(shrink=False): self.logger.warning( @@ -1251,6 +1277,7 @@ class Reconciler: return ret def sync_addresses(self): + """Synchronize the VPP Dataplane configuration for interface addresses""" for ifname in interface.get_interfaces(self.cfg) + loopback.get_loopbacks( self.cfg ): @@ -1279,6 +1306,7 @@ class Reconciler: return True def sync_admin_state(self): + """Synchronize the VPP Dataplane configuration for interface admin state""" for ifname in interface.get_interfaces(self.cfg) + loopback.get_loopbacks( self.cfg ): diff --git a/vpp/vppapi.py b/vpp/vppapi.py index d2501ad..decc18d 100644 --- a/vpp/vppapi.py +++ b/vpp/vppapi.py @@ -1,3 +1,17 @@ +# +# 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. +# +# -*- coding: utf-8 -*- """ The functions in this file interact with the VPP API to retrieve certain interface metadata. Its base class will never change state. See the @@ -12,6 +26,7 @@ from vpp_papi import VPPApiClient class VPPApi: + """The VPPApi class is a base class that abstracts the vpp_papi.""" def __init__(self, address="/run/vpp/api.sock", clientname="vppcfg"): self.logger = logging.getLogger("vppcfg.vppapi") self.logger.addHandler(logging.NullHandler()) @@ -25,6 +40,7 @@ class VPPApi: self.lcp_enabled = False def connect(self): + """ Connect to the VPP Dataplane, if we're not already connected """ if self.connected: return True @@ -56,6 +72,7 @@ class VPPApi: return True def disconnect(self): + """Disconnect from the VPP dataplane, if we are still connected.""" if not self.connected: return True self.vpp.disconnect() @@ -64,6 +81,7 @@ class VPPApi: return True def cache_clear(self): + """ Remove the cached VPP configuration elements and return an empty dictionary""" self.cache_read = False return { "lcps": {}, @@ -79,7 +97,7 @@ class VPPApi: } def cache_remove_lcp(self, lcpname): - """Removes the LCP and TAP interface, identified by lcpname, from the config.""" + """Removes the LCP and TAP interface, identified by lcpname, from the VPP config cache""" for _idx, lcp in self.cache["lcps"].items(): if lcp.host_if_name == lcpname: ifname = self.cache["interfaces"][lcp.host_sw_if_index].interface_name @@ -91,7 +109,7 @@ class VPPApi: return False def cache_remove_bondethernet_member(self, ifname): - """Removes the bonderthernet member interface, identified by name, from the config.""" + """Removes the bonderthernet member interface, identified by name, from the VPP config cache""" if not ifname in self.cache["interface_names"]: self.logger.warning( f"Trying to remove a bondethernet member interface which is not in the config: {ifname}" @@ -106,6 +124,7 @@ class VPPApi: return True def cache_remove_l2xc(self, ifname): + """Remvoes the l2xc from the VPP config cache""" if not ifname in self.cache["interface_names"]: self.logger.warning( f"Trying to remove an L2XC which is not in the config: {ifname}" @@ -116,6 +135,7 @@ class VPPApi: return True def cache_remove_vxlan_tunnel(self, ifname): + """Removes a vxlan_tunnel from the VPP config cache""" if not ifname in self.cache["interface_names"]: self.logger.warning( f"Trying to remove a VXLAN Tunnel which is not in the config: {ifname}" @@ -127,7 +147,7 @@ class VPPApi: return True def cache_remove_interface(self, ifname): - """Removes the interface, identified by name, from the config.""" + """Removes the interface, identified by name, from the VPP config cache""" if not ifname in self.cache["interface_names"]: self.logger.warning( f"Trying to remove an interface which is not in the config: {ifname}" @@ -154,6 +174,8 @@ class VPPApi: return True def readconfig(self): + """Read the configuration out of a running VPP Dataplane and put it into a + VPP config cache""" # pylint: disable=no-member if not self.connected and not self.connect(): self.logger.error("Could not connect to VPP") @@ -257,6 +279,7 @@ class VPPApi: return ret def get_sub_interfaces(self): + """Return all interfaces which have a sub-id and one or more tags""" subints = [ self.cache["interfaces"][x].interface_name for x in self.cache["interfaces"] @@ -266,6 +289,7 @@ class VPPApi: return subints def get_qinx_interfaces(self): + """Return all interfaces which have a sub-id and a non-zero inner vlan tag""" qinx_subints = [ self.cache["interfaces"][x].interface_name for x in self.cache["interfaces"] @@ -275,6 +299,7 @@ class VPPApi: return qinx_subints def get_dot1x_interfaces(self): + """Return all interfaces which have only an outer vlan tag (dot1q/dot1ad)""" dot1x_subints = [ self.cache["interfaces"][x].interface_name for x in self.cache["interfaces"] @@ -284,6 +309,7 @@ class VPPApi: return dot1x_subints def get_loopbacks(self): + """Return all interfaces of VPP type 'Loopback'""" loopbacks = [ self.cache["interfaces"][x].interface_name for x in self.cache["interfaces"] @@ -292,6 +318,8 @@ class VPPApi: return loopbacks def get_phys(self): + """Return all interfaces for which the super interface has the same sw_if_index + and aren't known to be virtual interfaces""" phys = [ self.cache["interfaces"][x].interface_name for x in self.cache["interfaces"] @@ -303,6 +331,7 @@ class VPPApi: return phys def get_bondethernets(self): + """Return all bondethernet interfaces""" bonds = [ self.cache["bondethernets"][x].interface_name for x in self.cache["bondethernets"] @@ -310,6 +339,7 @@ class VPPApi: return bonds def get_vxlan_tunnels(self): + """Return all vxlan_tunnel interfaces""" vxlan_tunnels = [ self.cache["interfaces"][x].interface_name for x in self.cache["interfaces"] @@ -318,6 +348,7 @@ class VPPApi: return vxlan_tunnels def get_lcp_by_interface(self, sw_if_index): + """Return the LCP config cache for the interface given by sw_if_index""" for _idx, lcp in self.cache["lcps"].items(): if lcp.phy_sw_if_index == sw_if_index: return lcp diff --git a/vppcfg b/vppcfg index 16bbc95..479d411 100755 --- a/vppcfg +++ b/vppcfg @@ -30,6 +30,7 @@ except ImportError: def main(): + """ The main vppcfg program """ parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) parser.add_argument( "-d", @@ -175,7 +176,7 @@ def main(): sys.exit(0) reconciler = Reconciler(cfg) - if not reconciler.vpp_readconfig(): + if not reconciler.vpp.readconfig(): sys.exit(-3) if not reconciler.phys_exist_in_vpp():