Skip to content

Commit

Permalink
IPv6 support for internal networks
Browse files Browse the repository at this point in the history
With this patch we're supporting IPv6 party in our infrastructure, at
least for internal networks. This enables the v6 address family on our
VRFs (if there is an IPv6 subnet connected to the router) and we
configure v6 ip addresses on interfaces where needed.

For BGPVPN we currently skip all v6 subnets. For extraroutes there is
still testing to be done.
  • Loading branch information
sebageek committed Jun 5, 2024
1 parent 1e48acc commit d9c6833
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 18 deletions.
10 changes: 9 additions & 1 deletion asr1k_neutron_l3/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import socket
import struct

from netaddr import IPNetwork, IPAddress
from netaddr import AddrFormatError, IPNetwork, IPAddress
from oslo_log import log as logging


Expand Down Expand Up @@ -128,6 +128,14 @@ def ip_in_network(ip, net):
return IPAddress(ip) in IPNetwork(net)


def get_ip_version(ip):
try:
ip = IPAddress(ip)
return ip.version
except AddrFormatError:
return None


def network_in_network(net, parent_net):
"""Check if a network is contained inside another network (also true for net == parent_net)"""
return IPNetwork(net) in IPNetwork(parent_net)
Expand Down
33 changes: 31 additions & 2 deletions asr1k_neutron_l3/models/netconf_yang/l3_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ class L3Constants(object):
PRIMARY = "primary"
SECONDARY = "secondary"
MASK = "mask"
IPV6 = "ipv6"
PREFIX = 'prefix'
PREFIX_LIST = 'prefix-list'
VRF = "vrf"
FORWARDING = "forwarding"
SHUTDOWN = "shutdown"
Expand Down Expand Up @@ -135,6 +138,7 @@ def __parameters__(cls):
{'key': 'ip_address', 'yang-path': 'ip/address', 'yang-key': "primary", 'type': VBIPrimaryIpAddress},
{'key': 'secondary_ip_addresses', 'yang-path': 'ip/address', 'yang-key': "secondary",
'type': [VBISecondaryIpAddress], 'default': [], 'validate':False},
{'key': 'ipv6_addresses', 'yang-path': 'ipv6/address', 'yang-key': "prefix-list", 'type': [VBIIpv6Address]},
{'key': 'nat_inside', 'yang-key': 'inside', 'yang-path': 'ip/nat', 'default': False,
'yang-type': YANG_TYPE.EMPTY},
{'key': 'nat_outside', 'yang-key': 'outside', 'yang-path': 'ip/nat', 'default': False,
Expand Down Expand Up @@ -216,11 +220,22 @@ def to_dict(self, context):
}
}
}
vbi[L3Constants.IP] = ip

if self.ipv6_addresses:
vbi[L3Constants.IPV6] = {
xml_utils.OPERATION: NC_OPERATION.PUT,
L3Constants.ADDRESS: {
L3Constants.PREFIX_LIST: [
addr.to_dict(context) for addr in self.ipv6_addresses
]
}
}
else:
vbi[L3Constants.IPV6] = {xml_utils.OPERATION: NC_OPERATION.REMOVE}

vrf = OrderedDict()
vrf[L3Constants.FORWARDING] = self.vrf

vbi[L3Constants.IP] = ip
vbi[L3Constants.VRF] = vrf

if context.version_min_17_3:
Expand Down Expand Up @@ -416,3 +431,17 @@ def to_dict(self, context):
ip[L3Constants.PRIMARY] = primary

return ip


class VBIIpv6Address(NyBase):
ITEM_KEY = L3Constants.PREFIX
LIST_KEY = L3Constants.PREFIX_LIST

@classmethod
def __parameters__(cls):
return [
{"key": 'prefix', 'id': True},
]

def to_dict(self, context):
return {L3Constants.PREFIX: self.prefix.lower()}
16 changes: 12 additions & 4 deletions asr1k_neutron_l3/models/netconf_yang/vrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from asr1k_neutron_l3.models.netconf_yang.l3_interface import VBInterface
from asr1k_neutron_l3.models.netconf_yang.nat import InterfaceDynamicNat
from asr1k_neutron_l3.models.netconf_yang.ny_base import NyBase, Requeable, NC_OPERATION, execute_on_pair, \
retry_on_failure
retry_on_failure, YANG_TYPE
from asr1k_neutron_l3.models.netconf_yang.route import VrfRoute

LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -129,6 +129,8 @@ def __parameters__(cls):
{'key': 'description'},
{'key': 'address_family_ipv4', "yang-key": "ipv4", "yang-path": "address-family",
'type': IpV4AddressFamily, "default": {}},
{'key': 'address_family_ipv6', "yang-key": "ipv6", "yang-path": "address-family",
'yang-type': YANG_TYPE.EMPTY},
{'key': 'rd'}
]

Expand All @@ -140,8 +142,10 @@ def __init__(self, **kwargs):
kwargs.get('rt_export', None) is not None:
self.address_family_ipv4 = IpV4AddressFamily(**kwargs)

self.asn = None
if kwargs.get('enable_ipv6'):
self.address_family_ipv6 = True

self.asn = None
if self.rd:
self.asn = self.rd.split(":")[0]

Expand All @@ -161,8 +165,12 @@ def to_dict(self, context):
# hopefully
definition[VrfConstants.RD] = self.rd

if self.address_family_ipv4 is not None:
definition[VrfConstants.ADDRESS_FAMILY][VrfConstants.IPV4] = self.address_family_ipv4.to_dict(context)
if self.address_family_ipv4 or self.address_family_ipv6:
af = definition[VrfConstants.ADDRESS_FAMILY] = {}
if self.address_family_ipv4 is not None:
af[VrfConstants.IPV4] = self.address_family_ipv4.to_dict(context)
if self.address_family_ipv6:
af[VrfConstants.IPV6] = ""

result = OrderedDict()
result[VrfConstants.DEFINITION] = definition
Expand Down
5 changes: 5 additions & 0 deletions asr1k_neutron_l3/models/neutron/l3/bgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ def __init__(self, vrf, asn=None, routable_interface=False, rt_export=[],
self.networks_v4 = set()

for net in connected_cidrs + extra_routes:
ip, _ = net.split("/", 1)
if utils.get_ip_version(ip) != 4:
# FIXME: skip ipv6 for now
continue

# rm is applied to all routable networks and their subnets
rm = None
if any(utils.network_in_network(net, routable_network) for routable_network in routable_networks):
Expand Down
32 changes: 24 additions & 8 deletions asr1k_neutron_l3/models/neutron/l3/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@

from asr1k_neutron_l3.common import utils
from asr1k_neutron_l3.models.neutron.l3 import base
from asr1k_neutron_l3.models.netconf_yang.l3_interface import VBInterface, VBIPrimaryIpAddress, VBISecondaryIpAddress
from asr1k_neutron_l3.models.netconf_yang.l3_interface import VBInterface, VBIPrimaryIpAddress, VBISecondaryIpAddress, \
VBIIpv6Address
from asr1k_neutron_l3.models.netconf_yang.l3_interface_state import VBInterfaceState

LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -63,6 +64,7 @@ def __init__(self, router_id, router_port, extra_atts):
self.vrf = utils.uuid_to_vrf_id(self.router_id)
self._primary_subnet_id = None
self.ip_address = self._ip_address()
self.ipv6_addresses = self._ipv6_addresses()

self.secondary_ip_addresses = []
self.primary_subnet = self._primary_subnet()
Expand All @@ -75,17 +77,27 @@ def __init__(self, router_id, router_port, extra_atts):
self.address_scope = router_port.get('address_scopes', {}).get('4')

def add_secondary_ip_address(self, ip_address, netmask):
# FIXME: is this method still in use?
self.secondary_ip_addresses.append(VBISecondaryIpAddress(address=ip_address,
mask=utils.to_netmask(netmask)))

def _ip_address(self):
if self.router_port.get('fixed_ips'):
n_fixed_ip = next(iter(self.router_port.get('fixed_ips')), None)
for n_fixed_ip in self.router_port.get('fixed_ips', []):
if utils.get_ip_version(n_fixed_ip['ip_address']) == 4:
self._primary_subnet_id = n_fixed_ip.get('subnet_id')

self._primary_subnet_id = n_fixed_ip.get('subnet_id')
return VBIPrimaryIpAddress(address=n_fixed_ip.get('ip_address'),
mask=utils.to_netmask(n_fixed_ip.get('prefixlen')))
return None

return VBIPrimaryIpAddress(address=n_fixed_ip.get('ip_address'),
mask=utils.to_netmask(n_fixed_ip.get('prefixlen')))
def _ipv6_addresses(self):
ipv6_addrs = []
for ip in self.router_port.get('fixed_ips', []):
if utils.get_ip_version(ip['ip_address']) == 6:
if self._primary_subnet_id is None:
self._primary_subnet_id = ip.get('subnet_id')
ipv6_addrs.append(VBIIpv6Address(prefix=f"{ip['ip_address']}/{ip['prefixlen']}"))
return ipv6_addrs

def _primary_subnet(self):
for subnet in self.router_port.get('subnets', []):
Expand Down Expand Up @@ -136,7 +148,7 @@ def __init__(self, router_id, router_port, extra_atts, dynamic_nat_pool):

self._rest_definition = VBInterface(name=self.bridge_domain, description=description,
mac_address=self.mac_address, mtu=self.mtu, vrf=self.vrf,
ip_address=self.ip_address,
ip_address=self.ip_address, ipv6_addresses=self.ipv6_addresses,
secondary_ip_addresses=self.secondary_ip_addresses, nat_outside=True,
redundancy_group=None, route_map='EXT-TOS', access_group_out='EXT-TOS',
ntp_disable=True, arp_timeout=cfg.CONF.asr1k_l3.external_iface_arp_timeout)
Expand All @@ -149,6 +161,10 @@ def _ip_address(self):
start_ip, end_ip = ips.split("-")
ip_pool = netaddr.IPSet(netaddr.IPRange(start_ip, end_ip))
for n_fixed_ip in self.router_port['fixed_ips']:
# filter out v6
if utils.get_ip_version(n_fixed_ip['ip_address']) != 4:
continue

if n_fixed_ip['ip_address'] not in ip_pool:
break
else:
Expand Down Expand Up @@ -182,7 +198,7 @@ def __init__(self, router_id, router_port, extra_atts):

self._rest_definition = VBInterface(name=self.bridge_domain, description=description,
mac_address=self.mac_address, mtu=self.mtu, vrf=self.vrf,
ip_address=self.ip_address,
ip_address=self.ip_address, ipv6_addresses=self.ipv6_addresses,
secondary_ip_addresses=self.secondary_ip_addresses,
nat_inside=True, redundancy_group=None, route_map="pbr-{}".format(self.vrf),
ntp_disable=True, arp_timeout=cfg.CONF.asr1k_l3.internal_iface_arp_timeout)
Expand Down
7 changes: 6 additions & 1 deletion asr1k_neutron_l3/models/neutron/l3/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,11 @@ def __init__(self, router_info):
LOG.error("Router %s has no rd attached, configuration is likely to fail!",
self.router_info.get('id'))

self.enable_ipv6 = any(iface.ipv6_addresses for iface in self.interfaces.all_interfaces)
self.vrf = vrf.Vrf(self.router_info.get('id'), description=description, asn=self.config.asr1k_l3.fabric_asn,
rd=self.router_atts.get('rd'), routable_interface=self.routable_interface,
rt_import=self.rt_import, rt_export=self.rt_export, global_vrf_id=global_vrf_id)
rt_import=self.rt_import, rt_export=self.rt_export, global_vrf_id=global_vrf_id,
enable_ipv6=self.enable_ipv6)

self.nat_acl = self._build_nat_acl()
self.pbr_acl = self._build_pbr_acl()
Expand Down Expand Up @@ -157,6 +159,7 @@ def _build_routes(self):
# In case the customer sets a default route, we will restrain from programming the openstack primary route
primary_overridden = False
for l3_route in self.router_info.get('routes', []):
# FIXME: another place where we need to support ipv6 routes
ip, netmask = utils.from_cidr(l3_route.get('destination'))
if netmask == '0.0.0.0':
primary_overridden = True
Expand All @@ -178,6 +181,7 @@ def _build_nat_acl(self):
for interface in self.address_scope_matches():
subnet = interface.primary_subnet

# FIXME: filter for ipv6?
if subnet is not None and subnet.get('cidr') is not None:
ip, netmask = utils.from_cidr(subnet.get('cidr'))
wildcard = utils.to_wildcard_mask(netmask)
Expand Down Expand Up @@ -456,6 +460,7 @@ def diff(self):
if not route_diff.valid:
diff_results['route'] = route_diff.to_dict()

# FIXME: this diff probably doesn't work for internal routers
snat_mode = constants.SNAT_MODE_POOL if self.use_nat_pool else constants.SNAT_MODE_INTERFACE
dynamic_nat_diff = self.dynamic_nat.get(snat_mode).diff()
if not dynamic_nat_diff.valid:
Expand Down
7 changes: 5 additions & 2 deletions asr1k_neutron_l3/models/neutron/l3/vrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

class Vrf(base.Base):
def __init__(self, name, description=None, asn=None, rd=None, routable_interface=False,
rt_import=[], rt_export=[], global_vrf_id=None):
rt_import=[], rt_export=[], global_vrf_id=None, enable_ipv6=False):
super(Vrf, self).__init__()
self.name = utils.uuid_to_vrf_id(name)
self.description = description
Expand All @@ -45,10 +45,13 @@ def __init__(self, name, description=None, asn=None, rd=None, routable_interface
self.rt_import = [{'asn_ip': asn_ip} for asn_ip in rt_import] if rt_import else None
self.rt_export = [{'asn_ip': asn_ip} for asn_ip in rt_export] if rt_export else None

self.enable_ipv6 = enable_ipv6

self._rest_definition = vrf.VrfDefinition(name=self.name, description=self.description,
rd=self.rd, enable_bgp=self.enable_bgp,
map=self.map, map_17_3=self.map_17_3,
rt_import=self.rt_import, rt_export=self.rt_export)
rt_import=self.rt_import, rt_export=self.rt_export,
enable_ipv6=enable_ipv6)

def get(self):
return vrf.VrfDefinition.get(self.name)
99 changes: 99 additions & 0 deletions asr1k_neutron_l3/tests/unit/models/netconf_yang/test_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from asr1k_neutron_l3.models.netconf_yang.arp_cache import ArpCache
from asr1k_neutron_l3.models.netconf_yang import bgp
from asr1k_neutron_l3.models.netconf_yang.l2_interface import BridgeDomain
from asr1k_neutron_l3.models.netconf_yang.l3_interface import VBInterface
from asr1k_neutron_l3.models.netconf_yang.vrf import VrfDefinition
from asr1k_neutron_l3.models.netconf_yang.nat import StaticNatList

Expand Down Expand Up @@ -408,3 +409,101 @@ def test_parse_nat_garp_flag(self):
snl = StaticNatList.from_xml(xml, context)
nat = snl.static_nats[0]
self.assertEqual('6657', nat.garp_bdvif_iface)

def test_bdvif_ipv6_parsing(self):
xml = """
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0"
message-id="urn:uuid:37bffcac-d037-48c6-b382-f29aaeddaa4a">
<data>
<native xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-native">
<interface>
<BD-VIF>
<name>5514</name>
<description>Dat interface geraet</description>
<mac-address>fa16.3e5a.9da7</mac-address>
<vrf>
<forwarding>c4505a0be48e497b942b0e07bb57f1fe</forwarding>
</vrf>
<ip>
<address>
<primary>
<address>192.168.1.1</address>
<mask>255.255.255.0</mask>
</primary>
</address>
<policy>
<route-map>pbr-c4505a0be48e497b942b0e07bb57f1fe</route-map>
</policy>
<nat xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-nat">
<stick/>
</nat>
</ip>
<ipv6>
<address>
<prefix-list>
<prefix>FD00::/64</prefix>
</prefix-list>
<prefix-list>
<prefix>FD00::256/64</prefix>
</prefix-list>
</address>
</ipv6>
<mtu>8950</mtu>
<ntp xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-ntp">
<disable/>
</ntp>
</BD-VIF>
</interface>
</native>
</data>
</rpc-reply>"""

expected_v6 = {"FD00::/64", "FD00::256/64"}

# parse
context = FakeASR1KContext()
iface = VBInterface.from_xml(xml, context)

self.assertEqual("5514", iface.name)
self.assertEqual("192.168.1.1", iface.ip_address.address)
self.assertEqual(expected_v6, {addr.prefix for addr in iface.ipv6_addresses})

# back to xml
iface_dict = iface.to_dict(context)
self.assertEqual(expected_v6, {p['prefix'] for p in iface_dict['BD-VIF']['ipv6']['address']['prefix-list']})

def test_vrf_ipv6_af_parsing(self):
xml = """
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0"
message-id="urn:uuid:37bffcac-d037-48c6-b382-f29aaeddaa4a">
<data>
<native xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-native">
<vrf>
<definition>
<name>c4505a0be48e497b942b0e07bb57f1fe</name>
<description>Router c4505a0b-e48e-497b-942b-0e07bb57f1fe</description>
<rd>65148:39354</rd>
<address-family>
<ipv4>
<export>
<map>exp-c4505a0be48e497b942b0e07bb57f1fe</map>
</export>
</ipv4>
<ipv6/>
</address-family>
</definition>
</vrf>
</native>
</data>
</rpc-reply>
"""

# parse
context = FakeASR1KContext()
vrf = VrfDefinition.from_xml(xml, context)
self.assertIsNotNone(vrf.address_family_ipv6)

# back to xml
vrf_dict = vrf.to_dict(context)
self.assertIsNone(vrf_dict['definition']['address-family']['ipv6'])
Loading

0 comments on commit d9c6833

Please sign in to comment.