From 597981e79b1acbe51e5042c3fcea4e38b5868bb3 Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Mon, 16 Jan 2023 10:15:57 +0000 Subject: [PATCH] Add prefixlist (mixed IPv4 and IPv6, containing either IP addresses or prefixes + tests --- vppcfg/config/__init__.py | 2 + vppcfg/config/prefixlist.py | 104 +++++++++++++++++++++++++++ vppcfg/config/test_prefixlist.py | 77 ++++++++++++++++++++ vppcfg/example.yaml | 12 ++++ vppcfg/schema.yaml | 6 ++ vppcfg/unittest/test_prefixlist.yaml | 26 +++++++ 6 files changed, 227 insertions(+) create mode 100644 vppcfg/config/prefixlist.py create mode 100644 vppcfg/config/test_prefixlist.py create mode 100644 vppcfg/unittest/test_prefixlist.yaml diff --git a/vppcfg/config/__init__.py b/vppcfg/config/__init__.py index 0bd0cbc..e8221e8 100644 --- a/vppcfg/config/__init__.py +++ b/vppcfg/config/__init__.py @@ -38,6 +38,7 @@ from .interface import validate_interfaces from .bridgedomain import validate_bridgedomains from .vxlan_tunnel import validate_vxlan_tunnels from .tap import validate_taps +from .prefixlist import validate_prefixlists from .acl import validate_acls @@ -90,6 +91,7 @@ class Validator: validate_bridgedomains, validate_vxlan_tunnels, validate_taps, + validate_prefixlists, validate_acls, ] diff --git a/vppcfg/config/prefixlist.py b/vppcfg/config/prefixlist.py new file mode 100644 index 0000000..eff8ad9 --- /dev/null +++ b/vppcfg/config/prefixlist.py @@ -0,0 +1,104 @@ +# +# Copyright (c) 2023 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. +# +""" A vppcfg configuration module that validates prefixlists """ +import logging +import socket +import ipaddress + + +def get_prefixlists(yaml): + """Return a list of all prefixlists.""" + ret = [] + if "prefixlists" in yaml: + for plname, _pl in yaml["prefixlists"].items(): + ret.append(plname) + return ret + + +def get_by_name(yaml, plname): + """Return the prefixlist by name, if it exists. Return None otherwise.""" + try: + if plname in yaml["prefixlists"]: + return plname, yaml["prefixlists"][plname] + except KeyError: + pass + return None, None + + +def count(yaml, plname): + """Return the number of IPv4 and IPv6 entries in the prefixlist. + Returns 0, 0 if it doesn't exist""" + v4, v6 = 0, 0 + + plname, pl = get_by_name(yaml, plname) + if not pl: + return 0, 0 + for m in pl["members"]: + ipn = ipaddress.ip_network(m, strict=False) + if ipn.version == 4: + v4 += 1 + elif ipn.version == 6: + v6 += 1 + + return v4, v6 + + +def count_ipv4(yaml, plname): + """Return the number of IPv4 entries in the prefixlist.""" + v4, v6 = count(yaml, plname) + return v4 + + +def count_ipv6(yaml, plname): + """Return the number of IPv6 entries in the prefixlist.""" + v4, v6 = count(yaml, plname) + return v6 + + +def has_ipv4(yaml, plname): + """Return True if the prefixlist has at least one IPv4 entry.""" + v4, v6 = count(yaml, plname) + return v4 > 0 + + +def has_ipv6(yaml, plname): + """Return True if the prefixlist has at least one IPv6 entry.""" + v4, v6 = count(yaml, plname) + return v6 > 0 + + +def is_empty(yaml, plname): + """Return True if the prefixlist has no entries.""" + v4, v6 = count(yaml, plname) + return v4 + v6 == 0 + + +def validate_prefixlists(yaml): + """Validate the semantics of all YAML 'prefixlists' entries""" + result = True + msgs = [] + logger = logging.getLogger("vppcfg.config") + logger.addHandler(logging.NullHandler()) + + if not "prefixlists" in yaml: + return result, msgs + + for plname, pl in yaml["prefixlists"].items(): + logger.debug(f"prefixlist {plname}: {pl}") + members = 0 + for pl_member in pl["members"]: + members += 1 + logger.debug(f"prefixlist {plname} member {members} is {pl_member}") + + return result, msgs diff --git a/vppcfg/config/test_prefixlist.py b/vppcfg/config/test_prefixlist.py new file mode 100644 index 0000000..5b1eab0 --- /dev/null +++ b/vppcfg/config/test_prefixlist.py @@ -0,0 +1,77 @@ +# +# 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 +from . import prefixlist +from .unittestyaml import UnitTestYaml + + +class TestACLMethods(unittest.TestCase): + def setUp(self): + with UnitTestYaml("test_prefixlist.yaml") as f: + self.cfg = yaml.load(f, Loader=yaml.FullLoader) + + def test_get_prefixlists(self): + plist = prefixlist.get_prefixlists(self.cfg) + self.assertIsInstance(plist, list) + self.assertEqual(5, len(plist)) + + def test_get_by_name(self): + plname, _pl = prefixlist.get_by_name(self.cfg, "trusted") + self.assertIsNotNone(_pl) + self.assertEqual("trusted", plname) + + plname, _pl = prefixlist.get_by_name(self.cfg, "pl-noexist") + self.assertIsNone(plname) + self.assertIsNone(_pl) + + def test_count(self): + v4, v6 = prefixlist.count(self.cfg, "trusted") + self.assertEqual(2, v4) + self.assertEqual(2, v6) + + v4, v6 = prefixlist.count(self.cfg, "empty") + self.assertEqual(0, v4) + self.assertEqual(0, v6) + + v4, v6 = prefixlist.count(self.cfg, "pl-noexist") + self.assertEqual(0, v4) + self.assertEqual(0, v6) + + def test_count_ipv4(self): + self.assertEqual(2, prefixlist.count_ipv4(self.cfg, "trusted")) + self.assertEqual(0, prefixlist.count_ipv4(self.cfg, "empty")) + self.assertEqual(0, prefixlist.count_ipv4(self.cfg, "pl-noexist")) + + def test_count_ipv6(self): + self.assertEqual(2, prefixlist.count_ipv6(self.cfg, "trusted")) + self.assertEqual(0, prefixlist.count_ipv6(self.cfg, "empty")) + self.assertEqual(0, prefixlist.count_ipv6(self.cfg, "pl-noexist")) + + def test_has_ipv4(self): + self.assertTrue(prefixlist.has_ipv4(self.cfg, "trusted")) + self.assertFalse(prefixlist.has_ipv4(self.cfg, "empty")) + self.assertFalse(prefixlist.has_ipv4(self.cfg, "pl-noexist")) + + def test_has_ipv6(self): + self.assertTrue(prefixlist.has_ipv6(self.cfg, "trusted")) + self.assertFalse(prefixlist.has_ipv6(self.cfg, "empty")) + self.assertFalse(prefixlist.has_ipv6(self.cfg, "pl-noexist")) + + def test_is_empty(self): + self.assertFalse(prefixlist.is_empty(self.cfg, "trusted")) + self.assertTrue(prefixlist.is_empty(self.cfg, "empty")) + self.assertTrue(prefixlist.is_empty(self.cfg, "pl-noexist")) diff --git a/vppcfg/example.yaml b/vppcfg/example.yaml index a7700f5..b54fdd8 100644 --- a/vppcfg/example.yaml +++ b/vppcfg/example.yaml @@ -118,6 +118,18 @@ taps: mtu: 1500 bridge: br1 +prefixlists: + trusted: + description: "All IPv4 and IPv6 trusted networks" + members: + - 192.0.2.1 + - 192.0.2.0/24 + - 2001:db8::1 + - 2001:db8::/64 + empty: + description: "An empty prefixlist" + members: [] + acls: acl01: description: "Test ACL" diff --git a/vppcfg/schema.yaml b/vppcfg/schema.yaml index 2bffd49..12d62db 100644 --- a/vppcfg/schema.yaml +++ b/vppcfg/schema.yaml @@ -4,6 +4,7 @@ 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) +prefixlists: map(include('prefixlist'),key=str(matches='[a-z][a-z0-9\-]+',min=1,max=64),required=False) acls: map(include('acl'),key=str(matches='[a-z][a-z0-9\-]+'),required=False) --- vxlan: @@ -81,8 +82,13 @@ tap: rx-ring-size: int(min=8,max=32768,required=False) tx-ring-size: int(min=8,max=32768,required=False) --- +prefixlist: + description: str(exclude='\'"',len=64,required=False) + members: list(any(ip_interface(),ip())) +--- # Valid: 80 "www" "-1024" "1024-" "1024-65535", and "any" acl-term-port-int-range-symbolic: any(int(min=1,max=65535),str(equals="any"),regex('^([1-9][0-9]*-|-[1-9][0-9]*|[1-9][0-9]*-[1-9][0-9]*)$'),regex('^[a-z][a-z0-9-]*$')) +--- # Valid: 80 "-245" "10-" "10-245", and "any" acl-term-icmp-int-range: any(int(min=0,max=255),str(equals="any"),regex('^([0-9]+-|-[1-9][0-9]*|[0-9]*-[1-9][0-9]*)$')) --- diff --git a/vppcfg/unittest/test_prefixlist.yaml b/vppcfg/unittest/test_prefixlist.yaml new file mode 100644 index 0000000..47e7739 --- /dev/null +++ b/vppcfg/unittest/test_prefixlist.yaml @@ -0,0 +1,26 @@ +prefixlists: + trusted: + description: "All IPv4 and IPv6 trusted networks" + members: + - 192.0.2.1 + - 192.0.2.0/24 + - 2001:db8::1 + - 2001:db8::/64 + deny-all: + description: "Default for IPv4 and IPv6" + members: + - 0.0.0.0/0 + - ::/0 + ipv4-only: + description: "Only contains IPv4" + members: + - 192.0.2.1 + - 192.0.2.0/24 + ipv6-only: + description: "Only contains IPv6" + members: + - 2001:db8::1 + - 2001:db8::/64 + empty: + description: "An empty list" + members: []