Add address.get_canonical() and is_canonical()

These functions will take either an IPv4/IPv6 address, or an IPv4/IPv6
prefix, and cast them to their canonical form. Notably for IPv6 addresses,
this means lower case and with the 0-tuples correctly formatted:

2001:DB8::1 becomes 2001:db8::1
2001:db8:0:0::1 becomes 2001:db8::1

This avoids spurious diffs in vppcfg when comparing to the output of the VPP
dataplane.
This commit is contained in:
Pim van Pelt
2023-05-25 18:24:46 +02:00
parent 9d1d8a32d6
commit abd3b2adbc
6 changed files with 89 additions and 4 deletions

View File

@ -113,3 +113,33 @@ def is_allowed(yaml, ifname, iface_addresses, ip_interface):
return False return False
return True return True
def get_canonical(iface_address):
"""Returns the canonical form of an interface address, which can be either an address
'2001:db8::1' or a prefix '2001:db8::1/64' for either IPv4 of IPv6.
This function the string representation of the canonical address."""
is_prefix = "/" in iface_address
if is_prefix:
iface_address, prefixlen = iface_address.split("/")
ip_address = ipaddress.ip_address(iface_address)
if is_prefix:
return str(ip_address) + "/" + prefixlen
return str(ip_address)
def is_canonical(iface_address):
"""Checks to see that the interface address is written in a canonical form, useful to
ensure that IPv6 addresses are written in such a way that they don't trigger spurious
diffs when comparing to VPP. As an example, '2001:DB8:0:0::1/64' is fine, but VPP will
show this as '2001:db8::1/64'.
Input can be either an address 2001:db8::1 or a prefix 2001:db8::1/128
This function returns False if the iface_address isn't canonical, and True otherwise.
"""
can = get_canonical(iface_address)
return can == iface_address

View File

@ -13,6 +13,7 @@
# #
""" A vppcfg configuration module that validates interfaces """ """ A vppcfg configuration module that validates interfaces """
import logging import logging
import ipaddress
from . import bondethernet from . import bondethernet
from . import bridgedomain from . import bridgedomain
from . import loopback from . import loopback
@ -500,6 +501,12 @@ def validate_interfaces(yaml):
f"interface {ifname} IP address {addr} conflicts with another" f"interface {ifname} IP address {addr} conflicts with another"
) )
result = False result = False
if not address.is_canonical(addr):
canonical = address.get_canonical(addr)
msgs.append(
f"interface {ifname} IP address {addr} is not canonical, use {canonical}"
)
result = False
if "l2xc" in iface: if "l2xc" in iface:
if has_sub(yaml, ifname): if has_sub(yaml, ifname):

View File

@ -0,0 +1,44 @@
#
# 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 addresses """
import unittest
import yaml
from . import address
class TestAddressMethods(unittest.TestCase):
def test_get_canonical(self):
self.assertEqual(address.get_canonical("0.0.0.0"), "0.0.0.0")
self.assertEqual(address.get_canonical("0.0.0.0/0"), "0.0.0.0/0")
self.assertEqual(address.get_canonical("192.168.1.1"), "192.168.1.1")
self.assertEqual(address.get_canonical("192.168.1.1/32"), "192.168.1.1/32")
self.assertEqual(address.get_canonical("2001:db8::1"), "2001:db8::1")
self.assertEqual(address.get_canonical("2001:db8::1/64"), "2001:db8::1/64")
self.assertEqual(address.get_canonical("2001:dB8::1/128"), "2001:db8::1/128")
self.assertEqual(address.get_canonical("2001:db8:0::1/128"), "2001:db8::1/128")
self.assertEqual(address.get_canonical("2001:db8::0:1"), "2001:db8::1")
def test_is_canonical(self):
self.assertTrue(address.is_canonical("0.0.0.0"))
self.assertTrue(address.is_canonical("0.0.0.0/0"))
self.assertTrue(address.is_canonical("192.168.1.1"))
self.assertTrue(address.is_canonical("192.168.1.1/32"))
self.assertTrue(address.is_canonical("2001:db8::1"))
self.assertTrue(address.is_canonical("2001:db8::1/64"))
self.assertFalse(address.is_canonical("2001:dB8::1/128")) # Capitals
self.assertFalse(address.is_canonical("2001:db8:0::1/128")) # Spurious 0
self.assertFalse(address.is_canonical("2001:db8::0:1")) # Spurious 0

View File

@ -20,7 +20,7 @@ interfaces:
lcp: "ice12-0-0" lcp: "ice12-0-0"
mac: f2:01:00:12:00:00 mac: f2:01:00:12:00:00
mtu: 9000 mtu: 9000
addresses: [ 192.0.2.17/30, 2001:db8:3::1/64 ] addresses: [ 192.0.2.17/30, 2001:DB8:3::1/64 ]
sub-interfaces: sub-interfaces:
1234: 1234:
mtu: 1200 mtu: 1200

View File

@ -13,7 +13,7 @@ interfaces:
GigabitEthernet1/0/0: GigabitEthernet1/0/0:
description: "Infra: nikhef-core-1.nl.switch.coloclue.net e1/34" description: "Infra: nikhef-core-1.nl.switch.coloclue.net e1/34"
lcp: e0-0 lcp: e0-0
addresses: [ 94.142.244.85/24, 2A02:898::146:1/64 ] addresses: [ 94.142.244.85/24, 2a02:898::146:1/64 ]
sub-interfaces: sub-interfaces:
100: 100:
description: "Cust: hvn0.nlams0.ipng.ch" description: "Cust: hvn0.nlams0.ipng.ch"

View File

@ -5,7 +5,8 @@ test:
- "interface .* IP address .* conflicts with another" - "interface .* IP address .* conflicts with another"
- "sub-interface .* IP address .* conflicts with another" - "sub-interface .* IP address .* conflicts with another"
- "loopback .* IP address .* conflicts with another" - "loopback .* IP address .* conflicts with another"
count: 14 - "interface .* IP address .* is not canonical, use .*"
count: 15
--- ---
interfaces: interfaces:
GigabitEthernet1/0/0: GigabitEthernet1/0/0:
@ -15,7 +16,7 @@ interfaces:
GigabitEthernet1/0/1: GigabitEthernet1/0/1:
lcp: e1-0-1 lcp: e1-0-1
addresses: [ 192.0.2.1/29, 2001:db8:1::1/64 ] addresses: [ 192.0.2.1/29, 2001:DB8:1::1/64 ]
sub-interfaces: sub-interfaces:
100: 100:
description: "These addresses overlap with Gi1/0/1" description: "These addresses overlap with Gi1/0/1"
@ -23,6 +24,9 @@ interfaces:
101: 101:
description: "These addresses overlap with loop0" description: "These addresses overlap with loop0"
addresses: [ 192.0.2.10/29, 2001:db8:2::2/64 ] addresses: [ 192.0.2.10/29, 2001:db8:2::2/64 ]
102:
description: "This address is not canonical"
addresses: [ 2001:DB8:5::1/64 ]
GigabitEthernet1/0/2: GigabitEthernet1/0/2:
lcp: e0-2 lcp: e0-2