Skip to content

Commit

Permalink
IPv6 support for external interfaces
Browse files Browse the repository at this point in the history
We can now configure external interfaces with IPv6, including DAPNet
support. This includes a rework of how the Router() class handles IP
addresses and offers route redistribution via route-maps + prefix lists
instead of explicit network statements.

The InterfaceList now has three new methods to get the list of IPs for a
router that are external, internal or routable. Routable IPs are defined
as the list of internal IPs where the address scope of the subnet
matches the address scope of the gateway (external) interface.

The BGP AddressFamily class now supports advertising of routes via
route-map. This means we have a route-map, which references prefix lists
that contain routable and internal IPs + their extra routes. For
routable IPs and their extraroutes we set a route target: routable IPs
get the cloud vrf's id, routable extraroutes have a prepended 1 to the
vrf id. This is very much the same as it is currently handled on the
device, though the config was pre-provisioned and only referenced via
route-maps on the respective network statements. This feature is
currently only enabled for IPv6 default and can be enabled for IPv4 with
the config option advertise_bgp_ipv4_routes_via_redistribute. The reason
of doing this is that editing the BGP config tree currently locks up the
whole device config due to requiring a full config resync.

Prefix lists referenced by route-maps that do not contain any prefix
don't appear as config on the device. The referencing route-map treats
non-existing prefix-lists as "always matching". As our expectation of
how this would work was empty prefix-list means "no match", we now have
to adjust our expectations of this feature and always add an entry if
the list would be empty otherwise. This is done via add_deny_if_empty,
which adds a deny for everything. The seq 4242 was deliberately chosen
to not collide with any of our existing rules, not because we saw any
problems with this, but because we didn't want to end up in a situation
where the device locks up because "there is already a rule with that
seq", even though we generally think this should work.
  • Loading branch information
sebageek committed Jan 17, 2025
1 parent 539fadb commit a8b064c
Show file tree
Hide file tree
Showing 13 changed files with 517 additions and 273 deletions.
4 changes: 4 additions & 0 deletions asr1k_neutron_l3/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@
cfg.BoolOpt('enable_fwaas_cleaning', default=True, help="Run FWaaS cleaning sync to remove stale FWaaS ACLs, "
"Class Maps and Service Policies"),
cfg.IntOpt('fwaas_cleaning_interval', default=300, help="Interval for FWaaS cleaning"),
cfg.BoolOpt('advertise_bgp_ipv4_routes_via_redistribute', default=False,
help=('Advertise BGP routes (BGPVPN/DAPNets) on IPv4 via redistribute static/connected + route-map '
'instead of using network statements. This avoids the global config lock, that occurs on current '
'firmwares (at least 17.15) when the BGP tree is modified.')),
]

ASR1K_L2_OPTS = [
Expand Down
77 changes: 57 additions & 20 deletions asr1k_neutron_l3/models/netconf_yang/bgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class BGPConstants(object):
VRF = "vrf"
REDISTRIBUTE = "redistribute"
REDISTRIBUTE_VRF = "redistribute-vrf"
REDISTRIBUTE_V6 = "redistribute-v6"
CONNECTED = "connected"
STATIC = "static"
UNICAST = "unicast"
Expand All @@ -45,6 +46,7 @@ class BGPConstants(object):
NUMBER = "number"
MASK = "mask"
ROUTE_MAP = "route-map"
DEFAULT = "default"


class AddressFamilyBase(NyBase):
Expand Down Expand Up @@ -106,6 +108,18 @@ def _wrapper_preamble(self, dict, context):
result = {BGPConstants.ROUTER: result}
return result

@classmethod
def _get(cls, **kwargs):
# make sure the result has a vrf associated
# due to the way we filter for our AF (with the ASN included) we always get a result, which
# prompts the original _get() method to always return a class, which results in problems when
# we want an empty result in case it's not there (should_be_none=True), therefore we filter out
# pseudoempty results here
result = super()._get(**kwargs)
if result and getattr(result, "vrf", None) is None:
return None
return result

def __init__(self, **kwargs):
super().__init__(**kwargs)

Expand Down Expand Up @@ -166,26 +180,38 @@ def __parameters__(cls):
{'key': 'vrf', 'yang-key': 'name'},
{'key': 'networks', 'yang-path': 'ipv4-unicast/network', 'yang-key': BGPConstants.WITH_MASK,
'type': [NetworkV4], 'default': []},
{'key': 'redistribute_connected_with_rm', 'yang-key': 'route-map',
'yang-path': 'ipv4-unicast/redistribute-vrf/connected'},
{'key': 'redistribute_static_with_rm', 'yang-key': 'route-map',
'yang-path': 'ipv4-unicast/redistribute-vrf/static/default'},
]

def to_dict(self, context):
if self.vrf is None:
return {}

vrf = {
BGPConstants.NAME: self.vrf,
BGPConstants.IPV4_UNICAST: {
xml_utils.OPERATION: NC_OPERATION.PUT,
BGPConstants.NETWORK: {
BGPConstants.WITH_MASK: [
net.to_dict(context) for net in sorted(self.networks, key=lambda x: (x.number, x.mask))
],
},
}
ipv4_unicast = {
xml_utils.OPERATION: NC_OPERATION.PUT,
BGPConstants.NETWORK: {
BGPConstants.WITH_MASK: [
net.to_dict(context) for net in sorted(self.networks, key=lambda x: (x.number, x.mask))
],
},
}
if self.redistribute_connected_with_rm or self.redistribute_static_with_rm:
redist = {}
if self.redistribute_connected_with_rm:
redist[BGPConstants.CONNECTED] = {BGPConstants.ROUTE_MAP: self.redistribute_connected_with_rm}
if self.redistribute_static_with_rm:
redist[BGPConstants.STATIC] = {
BGPConstants.DEFAULT: {BGPConstants.ROUTE_MAP: self.redistribute_static_with_rm}}
ipv4_unicast[BGPConstants.REDISTRIBUTE_VRF] = redist

result = {
BGPConstants.VRF: vrf,
BGPConstants.VRF: {
BGPConstants.NAME: self.vrf,
BGPConstants.IPV4_UNICAST: ipv4_unicast,
}
}
return result

Expand Down Expand Up @@ -223,24 +249,35 @@ def __parameters__(cls):
{'key': 'vrf', 'yang-key': 'name'},
{'key': 'networks', 'yang-path': BGPConstants.IPV6_UNICAST, 'yang-key': BGPConstants.NETWORK,
'type': [NetworkV6], 'default': []},
{'key': 'redistribute_connected_with_rm', 'yang-key': 'route-map',
'yang-path': 'ipv6-unicast/redistribute-v6/connected'},
{'key': 'redistribute_static_with_rm', 'yang-key': 'route-map',
'yang-path': 'ipv6-unicast/redistribute-v6/static'},
]

def to_dict(self, context):
if self.vrf is None:
return {}

vrf = {
BGPConstants.NAME: self.vrf,
BGPConstants.IPV6_UNICAST: {
xml_utils.OPERATION: NC_OPERATION.PUT,
BGPConstants.NETWORK: [
net.to_dict(context) for net in sorted(self.networks, key=lambda x: x.number)
],
}
ipv6_unicast = {
xml_utils.OPERATION: NC_OPERATION.PUT,
BGPConstants.NETWORK: [
net.to_dict(context) for net in sorted(self.networks, key=lambda x: x.number)
],
}
if self.redistribute_connected_with_rm or self.redistribute_static_with_rm:
redist = {}
if self.redistribute_connected_with_rm:
redist[BGPConstants.CONNECTED] = {BGPConstants.ROUTE_MAP: self.redistribute_connected_with_rm}
if self.redistribute_static_with_rm:
redist[BGPConstants.STATIC] = {BGPConstants.ROUTE_MAP: self.redistribute_static_with_rm}
ipv6_unicast[BGPConstants.REDISTRIBUTE_V6] = redist

result = {
BGPConstants.VRF: vrf,
BGPConstants.VRF: {
BGPConstants.NAME: self.vrf,
BGPConstants.IPV6_UNICAST: ipv6_unicast,
}
}
return result

Expand Down
6 changes: 6 additions & 0 deletions asr1k_neutron_l3/models/netconf_yang/l3_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,3 +463,9 @@ def __parameters__(cls):

def to_dict(self, context):
return {L3Constants.PREFIX: self.prefix.lower()}

@property
def address(self):
if self.prefix:
return self.prefix.split("/")[0]
return None
8 changes: 8 additions & 0 deletions asr1k_neutron_l3/models/netconf_yang/prefix.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ class PrefixBase(NyBase):
LIST_KEY = None
ITEM_KEY = PrefixConstants.PREFIX_LISTS

KNOWN_PREFIXES = [
"ext-", "snat-", "route-",
"routable4-", "routable6-",
"routable-extraroutes4-", "routable-extraroutes6-",
"internal4-", "internal6-",
"internal-extraroutes4-", "internal-extraroutes6-",
]

@classmethod
def __parameters__(cls):
return [
Expand Down
14 changes: 11 additions & 3 deletions asr1k_neutron_l3/models/netconf_yang/route_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ class RouteMap(NyBase):
LIST_KEY = None
ITEM_KEY = RouteMapConstants.ROUTE_MAP

KNOWN_PREFIXES = [
"exp-", "pbr-",
"bgp-redistribute4-", "bgp-redistribute6-",
]

@classmethod
def __parameters__(cls):
return [
Expand All @@ -81,8 +86,11 @@ def __init__(self, **kwargs):

@property
def neutron_router_id(self):
if self.name is not None and (self.name.startswith('exp-') or self.name.startswith('pbr-')):
return utils.vrf_id_to_uuid(self.name[4:])
if self.name:
for prefix in self.KNOWN_PREFIXES:
if self.name.startswith(prefix):
return utils.vrf_id_to_uuid(self.name[len(prefix):])
return None

def to_dict(self, context):
result = OrderedDict()
Expand Down Expand Up @@ -195,7 +203,7 @@ def to_dict(self, context):
entry[RouteMapConstants.IP] = {
RouteMapConstants.ADDRESS: {RouteMapConstants.PREFIX_LIST: self.prefix_list}}
if self.prefix_list_v6:
entry[RouteMapConstants.IP] = {
entry[RouteMapConstants.IPV6] = {
RouteMapConstants.ADDRESS: {RouteMapConstants.PREFIX_LIST: self.prefix_list_v6}}

if self.access_list is not None:
Expand Down
12 changes: 8 additions & 4 deletions asr1k_neutron_l3/models/neutron/l3/bgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@ class AddressFamilyBase(base.Base):
YANG_BGP_CLASS = None
YANG_BGP_NETWORK_CLASS = None

def __init__(self, vrf, asn=None, has_routable_interface=False, rt_export=[],
connected_cidrs=[], routable_networks=[], extra_routes=[]):
def __init__(self, vrf, asn=None, has_routable_interface=False, enable_af=True, rt_export=[],
redistribute_map=None, connected_cidrs=[], routable_networks=[], extra_routes=[]):
super().__init__()
self.vrf = utils.uuid_to_vrf_id(vrf)
self.enable_af = enable_af
self.has_routable_interface = has_routable_interface
self.asn = asn
self.enable_bgp = False
self.rt_export = rt_export
self.redistribute_map = redistribute_map
self.routable_networks = routable_networks
self.networks = set()

Expand All @@ -49,11 +51,13 @@ def __init__(self, vrf, asn=None, has_routable_interface=False, rt_export=[],
self.networks.add(net)
self.networks = list(self.networks)

if self.has_routable_interface or len(self.rt_export) > 0:
if self.enable_af and (self.has_routable_interface or len(self.rt_export) > 0):
self.enable_bgp = True

self._rest_definition = self.YANG_BGP_CLASS(vrf=self.vrf, asn=self.asn, enable_bgp=self.enable_bgp,
networks=self.networks)
networks=self.networks,
redistribute_static_with_rm=redistribute_map,
redistribute_connected_with_rm=redistribute_map)

def get(self):
return self.YANG_BGP_CLASS.get(self.vrf, asn=self.asn, enable_bgp=self.enable_bgp)
Expand Down
Loading

0 comments on commit a8b064c

Please sign in to comment.