From 6799026ca0281de06daf3cc47e0b7d90751cf15c Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Fri, 17 Jan 2025 15:50:40 +0100 Subject: [PATCH] IPv6 support for external interfaces 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. --- asr1k_neutron_l3/common/config.py | 4 + asr1k_neutron_l3/models/netconf_yang/bgp.py | 77 +++-- .../models/netconf_yang/l3_interface.py | 6 + .../models/netconf_yang/prefix.py | 8 + .../models/netconf_yang/route_map.py | 14 +- asr1k_neutron_l3/models/neutron/l3/bgp.py | 12 +- .../models/neutron/l3/interface.py | 149 ++++++---- asr1k_neutron_l3/models/neutron/l3/nat.py | 10 +- asr1k_neutron_l3/models/neutron/l3/prefix.py | 101 +++---- .../models/neutron/l3/route_map.py | 51 +++- asr1k_neutron_l3/models/neutron/l3/router.py | 270 ++++++++++-------- asr1k_neutron_l3/models/neutron/l3/vrf.py | 3 +- .../unit/models/netconf_yang/test_parsing.py | 89 ++++++ 13 files changed, 521 insertions(+), 273 deletions(-) diff --git a/asr1k_neutron_l3/common/config.py b/asr1k_neutron_l3/common/config.py index 36505909..691a578d 100644 --- a/asr1k_neutron_l3/common/config.py +++ b/asr1k_neutron_l3/common/config.py @@ -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 = [ diff --git a/asr1k_neutron_l3/models/netconf_yang/bgp.py b/asr1k_neutron_l3/models/netconf_yang/bgp.py index 1ba1b48d..0e32e2d4 100644 --- a/asr1k_neutron_l3/models/netconf_yang/bgp.py +++ b/asr1k_neutron_l3/models/netconf_yang/bgp.py @@ -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" @@ -45,6 +46,7 @@ class BGPConstants(object): NUMBER = "number" MASK = "mask" ROUTE_MAP = "route-map" + DEFAULT = "default" class AddressFamilyBase(NyBase): @@ -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) @@ -167,26 +181,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 @@ -224,24 +250,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 diff --git a/asr1k_neutron_l3/models/netconf_yang/l3_interface.py b/asr1k_neutron_l3/models/netconf_yang/l3_interface.py index db680d31..93e2aced 100644 --- a/asr1k_neutron_l3/models/netconf_yang/l3_interface.py +++ b/asr1k_neutron_l3/models/netconf_yang/l3_interface.py @@ -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 diff --git a/asr1k_neutron_l3/models/netconf_yang/prefix.py b/asr1k_neutron_l3/models/netconf_yang/prefix.py index 4c497f08..76929f57 100644 --- a/asr1k_neutron_l3/models/netconf_yang/prefix.py +++ b/asr1k_neutron_l3/models/netconf_yang/prefix.py @@ -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 [ diff --git a/asr1k_neutron_l3/models/netconf_yang/route_map.py b/asr1k_neutron_l3/models/netconf_yang/route_map.py index 63c02d43..a9123cf7 100644 --- a/asr1k_neutron_l3/models/netconf_yang/route_map.py +++ b/asr1k_neutron_l3/models/netconf_yang/route_map.py @@ -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 [ @@ -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() @@ -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: diff --git a/asr1k_neutron_l3/models/neutron/l3/bgp.py b/asr1k_neutron_l3/models/neutron/l3/bgp.py index 2688cc4a..3221ae33 100644 --- a/asr1k_neutron_l3/models/neutron/l3/bgp.py +++ b/asr1k_neutron_l3/models/neutron/l3/bgp.py @@ -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() @@ -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) diff --git a/asr1k_neutron_l3/models/neutron/l3/interface.py b/asr1k_neutron_l3/models/neutron/l3/interface.py index 72fa9c23..670ffcd6 100644 --- a/asr1k_neutron_l3/models/neutron/l3/interface.py +++ b/asr1k_neutron_l3/models/neutron/l3/interface.py @@ -52,59 +52,115 @@ def all_interfaces(self): return result + self.internal_interfaces + self.orphaned_interfaces + def get_internal_cidrs(self, ip_version): + cidrs = [] + for interface in self.internal_interfaces: + for subnet in interface.subnets: + if subnet.get('cidr') and utils.get_ip_version(subnet['cidr']) == ip_version: + cidrs.append(subnet['cidr']) + cidrs.sort() + return cidrs + + def get_internal_cidrs_v4(self): + return self.get_internal_cidrs(4) + + def get_internal_cidrs_v6(self): + return self.get_internal_cidrs(6) + + def get_external_cidrs(self, ip_version): + if not self.gateway_interface: + return [] + + return [subnet['cidr'] + for subnet in self.gateway_interface.subnets + if subnet.get('cidr') and utils.get_ip_version(subnet['cidr']) == ip_version] + + def get_external_cidrs_v4(self): + return self.get_external_cidrs(4) + + def get_external_cidrs_v6(self): + return self.get_external_cidrs(6) + + def get_routable_networks(self, ip_version): + if not self.gateway_interface: + return [] + + # routable networks are networks for which the subnet's subnet pool address scope + # matches the address scope of the external gateway + scope_key = {4: "address_scope_v4", 6: "address_scope_v6"}[ip_version] + + gw_scope_id = getattr(self.gateway_interface, scope_key, None) + if not gw_scope_id: + return [] + + subnets = [] + for interface in self.internal_interfaces: + for subnet in interface.subnets: + if subnet['address_scope_id'] == gw_scope_id and utils.get_ip_version(subnet['cidr']) == ip_version: + subnets.append(subnet['cidr']) + + return subnets + + def get_routable_networks_v4(self): + return self.get_routable_networks(4) + + def get_routable_networks_v6(self): + return self.get_routable_networks(6) + class Interface(base.Base): def __init__(self, router_id, router_port, extra_atts): - super(Interface, self).__init__() + super().__init__() - self.router_id = router_id self.router_port = router_port - self.extra_atts = extra_atts self.id = self.router_port.get('id') - self.bridge_domain = utils.to_bridge_domain(extra_atts.get('second_dot1q')) + self.router_id = router_id self.vrf = utils.uuid_to_vrf_id(self.router_id) - self._primary_subnet_id = None - self.ip_address = self._ip_address() - self.has_stateful_firewall = False - self.ipv6_addresses = self._ipv6_addresses() + self.extra_atts = extra_atts + self.bridge_domain = utils.to_bridge_domain(extra_atts.get('second_dot1q')) - self.secondary_ip_addresses = [] - self.primary_subnet = self._primary_subnet() - self.primary_gateway_ip = None - if self.primary_subnet is not None: - self.primary_gateway_ip = self.primary_subnet.get('gateway_ip') + self._primary_v4_subnet_id = None + self._primary_v6_subnet_id = None + self.gateway_ip_v4 = None + self.gateway_ip_v6 = None + self.address_scope_v4 = router_port.get('address_scopes', {}).get('4') + self.address_scope_v6 = router_port.get('address_scopes', {}).get('6') + self.secondary_ip_addresses = [] # NOTE(seba): I don't think this is being used + self.ipv4_address = self._ipv4_address() + self.ipv6_addresses = self._ipv6_addresses() + self._set_gateway_ips() self.mac_address = utils.to_cisco_mac(self.router_port.get('mac_address')) self.mtu = self.router_port.get('mtu') - self.address_scope = router_port.get('address_scopes', {}).get('4') + + self.has_stateful_firewall = False def add_secondary_ip_address(self, ip_address, netmask): self.secondary_ip_addresses.append(BDSecondaryIpAddress(address=ip_address, mask=utils.to_netmask(netmask))) - def _ip_address(self): + def _ipv4_address(self): 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_v4_subnet_id = n_fixed_ip.get('subnet_id') return BDPrimaryIpAddress(address=n_fixed_ip.get('ip_address'), mask=utils.to_netmask(n_fixed_ip.get('prefixlen'))) return None 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(BDIpv6Address(prefix=f"{ip['ip_address']}/{ip['prefixlen']}")) + for n_fixed_ip in self.router_port.get('fixed_ips', []): + if utils.get_ip_version(n_fixed_ip['ip_address']) == 6: + self._primary_v6_subnet_id = n_fixed_ip.get('subnet_id') + ipv6_addrs.append(BDIpv6Address(prefix=f"{n_fixed_ip['ip_address']}/{n_fixed_ip['prefixlen']}")) return ipv6_addrs - def _primary_subnet(self): + def _set_gateway_ips(self): for subnet in self.router_port.get('subnets', []): - if subnet.get('id') == self._primary_subnet_id: - return subnet - return None + if subnet['id'] == self._primary_v4_subnet_id: + self.gateway_ip_v4 = subnet['gateway_ip'] + elif subnet['id'] == self._primary_v6_subnet_id: + self.gateway_ip_v6 = subnet['gateway_ip'] @property def subnets(self): @@ -126,33 +182,23 @@ def delete(self): vbi = BDInterface(name=self.bridge_domain, vrf=self.vrf) return vbi.delete() - def disable_nat(self): - vbi = BDInterface(name=self.bridge_domain) - return vbi.disable_nat() - - def enable_nat(self): - vbi = BDInterface(name=self.bridge_domain) - return vbi.enable_nat() - class GatewayInterface(Interface): def __init__(self, router_id, router_port, extra_atts, dynamic_nat_pool): self.dynamic_nat_pool = dynamic_nat_pool - super(GatewayInterface, self).__init__(router_id, router_port, extra_atts) - - self.nat_address = self._nat_address() + super().__init__(router_id, router_port, extra_atts) # annotate details about the router to the interface description so this can be picked up by SNMP @property def _rest_definition(self): description = (f'type:gw;router:{self.router_id};network:{self.router_port["network_id"]};' - f'subnet:{self._primary_subnet_id}') + f'subnet:{self._primary_v4_subnet_id or self._primary_v6_subnet_id}') interface_args = dict(name=self.bridge_domain, description=description, mac_address=self.mac_address, mtu=self.mtu, vrf=self.vrf, - ip_address=self.ip_address, ipv6_addresses=self.ipv6_addresses, + ip_address=self.ipv4_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) @@ -165,9 +211,10 @@ def _rest_definition(self): return BDInterface(**interface_args) - def _ip_address(self): + def _ipv4_address(self): + # NOTE(seba): this method is only here to find the IPs not part of the dynamic nat pool if self.dynamic_nat_pool is None or not self.router_port.get('fixed_ips'): - return super()._ip_address() + return super()._ipv4_address() ips, _ = self.dynamic_nat_pool.split("/") start_ip, end_ip = ips.split("-") @@ -185,24 +232,15 @@ def _ip_address(self): self.vrf, self.dynamic_nat_pool) return None - self._primary_subnet_id = n_fixed_ip.get('subnet_id') + self._primary_v4_subnet_id = n_fixed_ip.get('subnet_id') return BDPrimaryIpAddress(address=n_fixed_ip['ip_address'], mask=utils.to_netmask(n_fixed_ip.get('prefixlen'))) - def _nat_address(self): - ips = self.router_port.get('fixed_ips') - if bool(ips): - for ip in ips: - address = ip.get('ip_address') - - if address != self.ip_address.address: - return address - class InternalInterface(Interface): def __init__(self, router_id, router_port, extra_atts, ingress_acl=None, egress_acl=None): - super(InternalInterface, self).__init__(router_id, router_port, extra_atts) + super().__init__(router_id, router_port, extra_atts) self.ingress_acl = ingress_acl self.egress_acl = egress_acl @@ -210,11 +248,12 @@ def __init__(self, router_id, router_port, extra_atts, ingress_acl=None, egress_ def _rest_definition(self): # annotate details about the router to the interface description so this can be picked up by SNMP description = (f'type:internal;project:{self.router_port["project_id"]};router:{self.router_id};' - f'network:{self.router_port["network_id"]};subnet:{self._primary_subnet_id}') + f'network:{self.router_port["network_id"]};' + f'subnet:{self._primary_v4_subnet_id or self._primary_v6_subnet_id}') interface_args = dict(name=self.bridge_domain, description=description, mac_address=self.mac_address, mtu=self.mtu, vrf=self.vrf, - ip_address=self.ip_address, ipv6_addresses=self.ipv6_addresses, + ip_address=self.ipv4_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, @@ -233,7 +272,7 @@ def _rest_definition(self): class OrphanedInterface(Interface): def __init__(self, router_id, router_port, extra_atts): - super(OrphanedInterface, self).__init__(router_id, router_port, extra_atts) + super().__init__(router_id, router_port, extra_atts) def update(self): return self.delete() diff --git a/asr1k_neutron_l3/models/neutron/l3/nat.py b/asr1k_neutron_l3/models/neutron/l3/nat.py index ac70983b..71f9247e 100644 --- a/asr1k_neutron_l3/models/neutron/l3/nat.py +++ b/asr1k_neutron_l3/models/neutron/l3/nat.py @@ -53,11 +53,9 @@ def _rest_definition(self): class DynamicNAT(BaseNAT): - def __init__(self, router_id, gateway_interface=None, interfaces=[], redundancy=None, mapping_id=None, + def __init__(self, router_id, gateway_interface=None, redundancy=None, mapping_id=None, mode=asr1k_constants.SNAT_MODE_POOL, bridge_domain=None): - super(DynamicNAT, self).__init__(router_id, gateway_interface, redundancy, mapping_id) - - self.interfaces = interfaces + super().__init__(router_id, gateway_interface, redundancy, mapping_id) self.specific_acl = True self.mode = mode @@ -145,7 +143,7 @@ def __init__(self, router_id, floating_ip, gateway_interface, redundancy=None, m self.floating_ip = floating_ip self.local_ip = floating_ip.get("fixed_ip_address") self.global_ip = floating_ip.get("floating_ip_address") - self.global_ip_mask = gateway_interface.ip_address.mask + self.global_ip_mask = gateway_interface.ipv4_address.mask self.bridge_domain = gateway_interface.bridge_domain self.id = "{},{}".format(self.local_ip, self.global_ip) self.mapping_id = utils.uuid_to_mapping_id(self.floating_ip.get('id')) @@ -168,7 +166,7 @@ def get(self): class ArpEntry(BaseNAT): def __init__(self, router_id, ip, gateway_interface): - super(ArpEntry, self).__init__(router_id, gateway_interface) + super().__init__(router_id, gateway_interface) self.ip = ip self.id = self.ip diff --git a/asr1k_neutron_l3/models/neutron/l3/prefix.py b/asr1k_neutron_l3/models/neutron/l3/prefix.py index a94ec260..69c3f3c8 100644 --- a/asr1k_neutron_l3/models/neutron/l3/prefix.py +++ b/asr1k_neutron_l3/models/neutron/l3/prefix.py @@ -13,8 +13,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -from operator import itemgetter, attrgetter - from asr1k_neutron_l3.models.neutron.l3 import base from asr1k_neutron_l3.common import utils @@ -22,48 +20,43 @@ class BasePrefix(base.Base): - IP_VERSION = None - - def __init__(self, name_prefix, router_id, gateway_interface, internal_interfaces): + def __init__(self, name_prefix, router_id, prefixes, add_deny_if_empty=False): self.vrf = utils.uuid_to_vrf_id(router_id) - self.internal_interfaces = internal_interfaces - self.gateway_interface = gateway_interface - self.gateway_address_scope = None - self.has_prefixes = False + self.prefixes = prefixes + self._rest_definition = self.PREFIX_MODEL(name=f"{name_prefix}-{self.vrf}") - if self.gateway_interface is not None: - self.gateway_address_scope = self.gateway_interface.address_scope + for n, pfx in enumerate(prefixes, 1): + self._rest_definition.add_seq( + self._make_seq(n * 10, pfx) + ) - self._rest_definition = self.PREFIX_MODEL(name="{}-{}".format(name_prefix, self.vrf)) + if add_deny_if_empty and not prefixes: + self._rest_definition.add_seq( + # use 4242 as a likely not-used seq no + self._make_seq(4242, self.DEFAULT, action="deny") + ) def diff(self, should_be_none=False): - return super().diff(should_be_none=not self.has_prefixes) + return super().diff(should_be_none=not self.prefixes) + + def _make_seq(self, no, cidr, action="permit"): + return prefix.PrefixSeq(no=no, action=action, ip=cidr) class BasePrefixV4(BasePrefix): PREFIX_MODEL = prefix.PrefixV4 - IP_VERSION = 4 + DEFAULT = "0.0.0.0/0" class BasePrefixV6(BasePrefix): PREFIX_MODEL = prefix.PrefixV6 - IP_VERSION = 6 + DEFAULT = "::/0" +# ext prefix, containing externally routed prefixes class ExtPrefixMixIn: - def __init__(self, router_id=None, gateway_interface=None, internal_interfaces=None): - super().__init__(name_prefix='ext', router_id=router_id, gateway_interface=gateway_interface, - internal_interfaces=internal_interfaces) - - if self.gateway_interface is not None: - i = 1 - for subnet in sorted(self.gateway_interface.subnets, key=itemgetter('id')): - if utils.get_ip_version(subnet.get('cidr')) != self.IP_VERSION: - continue - self.has_prefixes = True - self._rest_definition.add_seq( - prefix.PrefixSeq(no=i * 10, action="permit", ip=subnet.get('cidr'))) - i += 1 + def __init__(self, *args, **kwargs): + super().__init__(name_prefix='ext', *args, **kwargs) class ExtPrefixV4(ExtPrefixMixIn, BasePrefixV4): @@ -74,35 +67,25 @@ class ExtPrefixV6(ExtPrefixMixIn, BasePrefixV6): pass +# snat prefixes, list containing everything that should not be snatted class SnatPrefix(BasePrefixV4): - def __init__(self, router_id=None, gateway_interface=None, internal_interfaces=None): - super(SnatPrefix, self).__init__(name_prefix='snat', router_id=router_id, gateway_interface=gateway_interface, - internal_interfaces=internal_interfaces) - - i = 1 - for interface in sorted(self.internal_interfaces, key=attrgetter('id')): - for subnet in sorted(interface.subnets, key=itemgetter('id')): - if utils.get_ip_version(subnet.get('cidr')) != self.IP_VERSION: - continue - self.has_prefixes = True - self._rest_definition.add_seq( - prefix.PrefixSeq(no=i * 10, action="permit", ip=subnet.get('cidr'))) - i += 1 - - -class RoutePrefix(BasePrefixV4): - def __init__(self, router_id=None, gateway_interface=None, internal_interfaces=None): - super(RoutePrefix, self).__init__(name_prefix='route', router_id=router_id, gateway_interface=gateway_interface, - internal_interfaces=internal_interfaces) - - i = 1 - for interface in sorted(self.internal_interfaces, key=attrgetter('id')): - for subnet in sorted(interface.subnets, key=itemgetter('id')): - if utils.get_ip_version(subnet.get('cidr')) != self.IP_VERSION: - continue - self.has_prefixes = True - cidr = subnet.get('cidr') - permit_ge = utils.prefix_from_cidr(cidr) + 1 - self._rest_definition.add_seq( - prefix.PrefixSeq(no=i * 10, action="permit", ip=cidr, ge=permit_ge)) - i += 1 + def __init__(self, *args, **kwargs): + super().__init__(name_prefix='snat', *args, **kwargs) + + +# everything that should be routed +class RoutePrefixMixin: + def __init__(self, *args, **kwargs): + super().__init__(name_prefix='route', *args, **kwargs) + + def _make_seq(self, no, cidr, action="permit"): + permit_ge = utils.prefix_from_cidr(cidr) + 1 + return prefix.PrefixSeq(no=no, action=action, ip=cidr, ge=permit_ge) + + +class RoutePrefixV4(RoutePrefixMixin, BasePrefixV4): + pass + + +class RoutePrefixV6(RoutePrefixMixin, BasePrefixV6): + pass diff --git a/asr1k_neutron_l3/models/neutron/l3/route_map.py b/asr1k_neutron_l3/models/neutron/l3/route_map.py index ca4ab1a9..61d9d9e3 100644 --- a/asr1k_neutron_l3/models/neutron/l3/route_map.py +++ b/asr1k_neutron_l3/models/neutron/l3/route_map.py @@ -63,7 +63,7 @@ def get(self): class PBRRouteMap(base.Base): - def __init__(self, name, gateway_interface=None): + def __init__(self, name, has_gateway_interface=False): super(PBRRouteMap, self).__init__() self.vrf = utils.uuid_to_vrf_id(name) @@ -71,9 +71,56 @@ def __init__(self, name, gateway_interface=None): sequences = [] - if gateway_interface is not None: + if has_gateway_interface: sequences.append(route_map.MapSequence(seq_no=15, operation='permit', ip_precedence='routine')) self._rest_definition = route_map.RouteMap(name=self.name, seq=sequences) + + +class RedistRouteMapBase(base.Base): + def __init__(self, router_id, routable_rt, extraroutes_rt, enabled): + super().__init__() + + self.vrf = utils.uuid_to_vrf_id(router_id) + self.name = f"bgp-redistribute{self.IP_VERSION}-{self.vrf}" + self.enabled = enabled + routable_rt_list = [routable_rt] if routable_rt else None + extraroutes_rt_list = [extraroutes_rt] if extraroutes_rt else None + + sequences = [ + route_map.MapSequence( + seq_no=10, operation='permit', asn=routable_rt_list, + **{self.PREFIX_LIST_PARAM: f"routable{self.IP_VERSION}-{self.vrf}"}), + route_map.MapSequence( + seq_no=20, operation='permit', asn=extraroutes_rt_list, + **{self.PREFIX_LIST_PARAM: f"routable-extraroutes{self.IP_VERSION}-{self.vrf}"}), + route_map.MapSequence( + seq_no=30, operation='permit', + **{self.PREFIX_LIST_PARAM: f"internal{self.IP_VERSION}-{self.vrf}"}), + route_map.MapSequence( + seq_no=40, operation='permit', + **{self.PREFIX_LIST_PARAM: f"internal-extraroutes{self.IP_VERSION}-{self.vrf}"}), + ] + + self._rest_definition = route_map.RouteMap(name=self.name, seq=sequences) + + def diff(self, should_be_none=False): + return super().diff(should_be_none=not self.enabled) + + def update(self): + if self.enabled: + return super().update() + else: + return self.delete() + + +class Redist4RouteMap(RedistRouteMapBase): + IP_VERSION = "4" + PREFIX_LIST_PARAM = "prefix_list" + + +class Redist6RouteMap(RedistRouteMapBase): + IP_VERSION = "6" + PREFIX_LIST_PARAM = "prefix_list_v6" diff --git a/asr1k_neutron_l3/models/neutron/l3/router.py b/asr1k_neutron_l3/models/neutron/l3/router.py index be0c1028..3ab41642 100644 --- a/asr1k_neutron_l3/models/neutron/l3/router.py +++ b/asr1k_neutron_l3/models/neutron/l3/router.py @@ -17,7 +17,7 @@ import os from collections import defaultdict - +from oslo_config import cfg from oslo_log import log as logging from asr1k_neutron_l3.common import asr1k_constants as constants, utils @@ -52,52 +52,50 @@ def __init__(self, router_info): self.status = self.router_info.get('status') self.gateway_interface = None - self.router_id = self.router_info.get('id') + self.router_id = self.router_info['id'] self.interfaces = self._build_interfaces() self.routes = self._build_routes() self.enable_snat = False self.routable_interface = False if router_info.get('external_gateway_info') is not None: self.enable_snat = router_info.get('external_gateway_info', {}).get('enable_snat', False) - self.routable_interface = len(self.address_scope_matches()) > 0 + self.routable_interface = bool(self.interfaces.get_routable_networks_v4() or + self.interfaces.get_routable_networks_v6()) description = self.router_info.get('description') - - if description is None or len(description) == 0: - description = "Router {}".format(self.router_id) + if not description: + description = f"Router {self.router_id}" # TODO : get rt's from config for router address_scope_config = router_info.get(constants.ADDRESS_SCOPE_CONFIG, {}) - rt = None - global_vrf_id = None + self.rt = None + self.global_vrf_id = None if self.gateway_interface is not None: - if self.gateway_interface.address_scope in address_scope_config: - rt = address_scope_config[self.gateway_interface.address_scope] - global_vrf_id = self._to_global_vrf_id(rt) - elif self.gateway_interface.address_scope is not None: + # We excpect that the v4 and v6 adress scope is always from the same cloud VRF + gw_address_scope = self.gateway_interface.address_scope_v4 or self.gateway_interface.address_scope_v6 + if gw_address_scope in address_scope_config: + self.rt = address_scope_config[gw_address_scope] + self.global_vrf_id = self._to_global_vrf_id(self.rt) + elif gw_address_scope: LOG.error("Router %s has a gateway interface, but no address scope was found in config " "(address scope of router: %s, available scopes: %s)", - self.router_id, self.gateway_interface.address_scope, list(address_scope_config.keys())) + self.router_id, gw_address_scope, list(address_scope_config.keys())) if not self.router_atts.get('rd'): LOG.error("Router %s has no rd attached, configuration is likely to fail!", - self.router_info.get('id')) + self.router_id) - self.vrf = vrf.Vrf(self.router_info.get('id'), description=description, asn=self.config.asr1k_l3.fabric_asn, + self.vrf = vrf.Vrf(self.router_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=self.global_vrf_id, enable_ipv6=self.enable_ipv6) self.fwaas_conf, self.fwaas_external_policies = self._build_fwaas_conf() + self.route_maps = self._build_route_maps() self.nat_acl = self._build_nat_acl() - self.route_map = route_map.RouteMap(self.router_info.get('id'), rt=rt, - routable_interface=self.routable_interface, enable_ipv6=self.enable_ipv6) - - self.pbr_route_map = route_map.PBRRouteMap(self.router_info.get('id'), gateway_interface=self.gateway_interface) - self.bgp_address_family = self._build_bgp_address_family() self.dynamic_nat = self._build_dynamic_nat() @@ -118,26 +116,12 @@ def _to_global_vrf_id(self, rt): @property def enable_ipv4(self): - return any(iface.ip_address for iface in self.interfaces.all_interfaces) + return any(iface.ipv4_address for iface in self.interfaces.all_interfaces) @property def enable_ipv6(self): return any(iface.ipv6_addresses for iface in self.interfaces.all_interfaces) - def address_scope_matches(self): - result = [] - if self.gateway_interface is not None: - for interface in self.interfaces.internal_interfaces: - if self.gateway_interface.address_scope is not None: - if interface.address_scope == self.gateway_interface.address_scope: - result.append(interface) - result = sorted(result, key=lambda _iface: _iface.id) - return result - - def get_routable_networks(self): - return [iface.primary_subnet['cidr'] for iface in self.address_scope_matches() - if iface.primary_subnet and 'cidr' in iface.primary_subnet] - def _get_fwaas_acls_by_port(self): """ This method retrieves the ACLs associated with each port for the router. @@ -206,34 +190,31 @@ def _build_routes(self): if self._route_has_connected_interface(r): routes[ip_net.version].append(r) + # handle default routes if self.gateway_interface is not None: - if self.gateway_interface.primary_gateway_ip and not primary_overridden[4]: + if self.gateway_interface.gateway_ip_v4 and not primary_overridden[4]: primary_route = route.RouteV4(self.router_id, "0.0.0.0/0", - self.gateway_interface.primary_gateway_ip) + self.gateway_interface.gateway_ip_v4) if self._route_has_connected_interface(primary_route): routes[4].append(primary_route) - if self.gateway_interface.ipv6_addresses and not primary_overridden[6]: + if self.gateway_interface.gateway_ip_v6 and not primary_overridden[6]: primary_route = route.RouteV6(self.router_id, "::/0", - sorted(self.gateway_interface.ipv6_addresses)[0]) + self.gateway_interface.gateway_ip_v6) if self._route_has_connected_interface(primary_route): routes[6].append(primary_route) return routes def _build_nat_acl(self): - acl = access_list.AccessList("NAT-{}".format(utils.uuid_to_vrf_id(self.router_id))) + acl = access_list.AccessList(f"NAT-{utils.uuid_to_vrf_id(self.router_id)}") # Check address scope and deny any where internal interface matches external - 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) - rule = access_list.Rule(action='deny', source=ip, source_mask=wildcard) - acl.append_rule(rule) + for cidr in self.interfaces.get_routable_networks_v4(): + ip, netmask = utils.from_cidr(cidr) + wildcard = utils.to_wildcard_mask(netmask) + rule = access_list.Rule(action='deny', source=ip, source_mask=wildcard) + acl.append_rule(rule) if not self.enable_snat: acl.append_rule(access_list.Rule(action='deny')) @@ -242,59 +223,53 @@ def _build_nat_acl(self): return acl def _route_has_connected_interface(self, l3_route): - gw_port = self.router_info.get('gw_port', None) - if gw_port is not None: - - for subnet in gw_port.get('subnets'): - if subnet and subnet.get('cidr') and utils.ip_in_network(l3_route.nexthop, subnet['cidr']): - return True - - int_ports = self.router_info.get('_interfaces', []) - - for int_port in int_ports: - for subnet in int_port.get('subnets', []): - if subnet and subnet.get('cidr') and utils.ip_in_network(l3_route.nexthop, subnet['cidr']): - return True - - return False + all_cidrs = ( + self.interfaces.get_external_cidrs_v4() + + self.interfaces.get_external_cidrs_v6() + + self.interfaces.get_internal_cidrs_v4() + + self.interfaces.get_internal_cidrs_v6() + ) + return any(utils.ip_in_network(l3_route.nexthop, cidr) for cidr in all_cidrs) def _build_bgp_address_family(self): - networks_v4 = [iface.primary_subnet['cidr'] for iface in self.interfaces.internal_interfaces - if iface.primary_subnet and 'cidr' in iface.primary_subnet and - utils.get_ip_version(iface.primary_subnet['cidr']) == 4] - networks_v6 = [addr.prefix for iface in self.interfaces.internal_interfaces - for addr in iface.ipv6_addresses] - routable_networks_v4 = [iface.primary_subnet['cidr'] for iface in self.address_scope_matches() - if iface.primary_subnet and 'cidr' in iface.primary_subnet and - utils.get_ip_version(iface.primary_subnet['cidr']) == 4] - routable_networks_v6 = [addr.prefix for iface in self.address_scope_matches() - for addr in iface.ipv6_addresses] - - extra_routes_v4 = [] - extra_routes_v6 = [] - if self.router_info["bgpvpn_advertise_extra_routes"]: - extra_routes_v4 = [x.cidr for x in self.routes[4].routes if x.cidr != "0.0.0.0/0"] - extra_routes_v6 = [x.cidr for x in self.routes[6].routes if x.cidr != "::/0"] - - def_args = { - "vrf": utils.uuid_to_vrf_id(self.router_id), - "asn": self.config.asr1k_l3.fabric_asn, - "has_routable_interface": self.routable_interface, - "rt_export": self.rt_export, - } - return { - 4: bgp.AddressFamilyV4(connected_cidrs=networks_v4, extra_routes=extra_routes_v4, - routable_networks=routable_networks_v4, **def_args), - 6: bgp.AddressFamilyV6(connected_cidrs=networks_v6, extra_routes=extra_routes_v6, - routable_networks=routable_networks_v6, **def_args), - } + bgp_afs = {} + for ip_version, BGPAddressFamily in ((4, bgp.AddressFamilyV4), (6, bgp.AddressFamilyV6)): + if ip_version == 6 or ip_version == 4 and cfg.CONF.asr1k_l3.advertise_bgp_ipv4_routes_via_redistribute: + # use redistribute static/connected + networks = [] + routable_networks = [] + has_routable_interface = bool(self.interfaces.get_routable_networks(ip_version)) + redistribute_map = f"bgp-redistribute{ip_version}-{utils.uuid_to_vrf_id(self.router_id)}" + else: + # use network statements + networks = self.interfaces.get_internal_cidrs(ip_version) + routable_networks = self.interfaces.get_routable_networks(ip_version) + has_routable_interface = bool(routable_networks) + redistribute_map = None + + enable_af = self.enable_ipv4 if ip_version == 4 else self.enable_ipv6 + + extra_routes = [] + if self.router_info["bgpvpn_advertise_extra_routes"]: + extra_routes = [x.cidr for x in self.routes[ip_version].routes if x.cidr not in ("0.0.0.0/0", "::/0")] + + bgp_afs[ip_version] = BGPAddressFamily( + vrf=utils.uuid_to_vrf_id(self.router_id), + asn=self.config.asr1k_l3.fabric_asn, rt_export=self.rt_export, + connected_cidrs=networks, extra_routes=extra_routes, + routable_networks=routable_networks, + has_routable_interface=has_routable_interface, + redistribute_map=redistribute_map, + enable_af=enable_af, + ) + return bgp_afs def _build_dynamic_nat(self): pool_nat = nat.DynamicNAT(self.router_id, gateway_interface=self.gateway_interface, - interfaces=self.interfaces, mode=constants.SNAT_MODE_POOL, + mode=constants.SNAT_MODE_POOL, mapping_id=utils.uuid_to_mapping_id(self.router_id)) interface_nat = nat.DynamicNAT(self.router_id, gateway_interface=self.gateway_interface, - interfaces=self.interfaces, mode=constants.SNAT_MODE_INTERFACE) + mode=constants.SNAT_MODE_INTERFACE) return {constants.SNAT_MODE_POOL: pool_nat, constants.SNAT_MODE_INTERFACE: interface_nat} @@ -336,18 +311,77 @@ def _build_prefix_lists(self): result = [] # external interface - result.append(prefix.ExtPrefixV4(router_id=self.router_id, gateway_interface=self.gateway_interface)) - result.append(prefix.ExtPrefixV6(router_id=self.router_id, gateway_interface=self.gateway_interface)) + result.append(prefix.ExtPrefixV4(router_id=self.router_id, prefixes=self.interfaces.get_external_cidrs_v4())) + result.append(prefix.ExtPrefixV6(router_id=self.router_id, prefixes=self.interfaces.get_external_cidrs_v6())) + + result.append(prefix.SnatPrefix(router_id=self.router_id, prefixes=self.interfaces.get_routable_networks_v4())) + + result.append(prefix.RoutePrefixV4(router_id=self.router_id, + prefixes=self.interfaces.get_routable_networks_v4())) + result.append(prefix.RoutePrefixV6(router_id=self.router_id, + prefixes=self.interfaces.get_routable_networks_v6())) + + # the new prefix lists + # routable -> all dapnets + # routable-extraroutes -> all extraroutes part of a dapnets + # internal -> everything internal + # extraroutes -> extraroutes that are not routable + for ip_version, PrefixClass in ((4, prefix.BasePrefixV4), (6, prefix.BasePrefixV6)): + if ip_version == 4 and not cfg.CONF.asr1k_l3.advertise_bgp_ipv4_routes_via_redistribute: + # we don't need the lists for v4 if they're not being used + # to avoid potential config locks, we won't configure them + continue + + af_enabled = self.enable_ipv4 if ip_version == 4 else self.enable_ipv6 + routable_networks = self.interfaces.get_routable_networks(ip_version) + internal_networks = self.interfaces.get_internal_cidrs(ip_version) + routable_extraroutes = [] + internal_extraroutes = [] + for extraroute in self.routes[ip_version].routes: + net = extraroute.cidr + if net in ("0.0.0.0/0", "::/0"): + continue + + if any(utils.network_in_network(net, routable_network) for routable_network in routable_networks): + routable_extraroutes.append(net) + else: + internal_extraroutes.append(net) + + result.extend([ + PrefixClass(f"routable{ip_version}", self.router_id, routable_networks, add_deny_if_empty=af_enabled), + PrefixClass(f"routable-extraroutes{ip_version}", self.router_id, routable_extraroutes, + add_deny_if_empty=af_enabled), + PrefixClass(f"internal{ip_version}", self.router_id, internal_networks, add_deny_if_empty=af_enabled), + PrefixClass(f"internal-extraroutes{ip_version}", self.router_id, internal_extraroutes, + add_deny_if_empty=af_enabled), + ]) - no_snat_interfaces = self.address_scope_matches() + return result - result.append(prefix.SnatPrefix(router_id=self.router_id, gateway_interface=self.gateway_interface, - internal_interfaces=no_snat_interfaces)) + def _build_route_maps(self): + extraroutes_rt = None + if self.rt and ':' in self.rt: + # 65126:106 --> 65126:1106 + asn, sf = self.rt.split(":", 1) + extraroutes_rt = f"{asn}:1{sf}" - result.append(prefix.RoutePrefix(router_id=self.router_id, gateway_interface=self.gateway_interface, - internal_interfaces=no_snat_interfaces)) + route_maps = [ + # exp route-map + route_map.RouteMap(self.router_id, rt=self.rt, + routable_interface=self.routable_interface, enable_ipv6=self.enable_ipv6), - return result + # pbr route-map + route_map.PBRRouteMap(self.router_id, + has_gateway_interface=bool(self.gateway_interface)), + + # redistribute routemaps + route_map.Redist4RouteMap(self.router_id, self.rt, extraroutes_rt, + enabled=(self.enable_ipv4 and + cfg.CONF.asr1k_l3.advertise_bgp_ipv4_routes_via_redistribute)), + route_map.Redist6RouteMap(self.router_id, self.rt, extraroutes_rt, enabled=self.enable_ipv6), + ] + + return route_maps def _build_fwaas_conf(self): router_info = self.router_info @@ -420,18 +454,12 @@ def _update(self): for prefix_list in self.prefix_lists: results.append(prefix_list.update()) - results.append(self.route_map.update()) + for rm in self.route_maps: + results.append(rm.update()) results.append(self.vrf.update()) - if self.gateway_interface is not None: - results.append(self.pbr_route_map.update()) - else: - results.append(self.pbr_route_map.delete()) - - # results.append(self.bgp_address_family.update()) - for ip_version in (4, 6): - if (self.routable_interface or len(self.rt_export) > 0) and self.bgp_address_family[ip_version].networks: + if self.bgp_address_family[ip_version].enable_bgp: results.append(self.bgp_address_family[ip_version].update()) else: results.append(self.bgp_address_family[ip_version].delete()) @@ -501,7 +529,8 @@ def _delete(self): results.append(prefix.ExtPrefixV6(router_id=self.router_id).delete()) results.append(prefix.RoutePrefix(router_id=self.router_id).delete()) - results.append(self.route_map.delete()) + for rm in self.route_maps: + results.append(rm.delete()) results.append(self.floating_ips.delete()) results.append(self.arp_entries.delete()) results.append(self.routes[4].delete()) @@ -511,7 +540,6 @@ def _delete(self): results.append(self.dynamic_nat.get(key).delete()) results.append(self.nat_pool.delete()) - results.append(self.pbr_route_map.delete()) results.append(self.nat_acl.delete()) results.append(self.bgp_address_family[4].delete()) results.append(self.bgp_address_family[6].delete()) @@ -551,14 +579,10 @@ def diff(self): diff_results['prefix_list'] = [] diff_results['prefix_list'].append(prefix_diff.to_dict()) - rm_diff = self.route_map.diff() - if not rm_diff.valid: - diff_results['route_map'] = rm_diff.to_dict() - - if self.gateway_interface: - pbr_rm_diff = self.pbr_route_map.diff() - if not pbr_rm_diff.valid: - diff_results['pbr_route_map'] = pbr_rm_diff.to_dict() + for rm in self.route_maps: + rm_diff = rm.diff() + if not rm_diff.valid: + diff_results[f'route_map-{rm.name}'] = rm_diff.to_dict() for ip_version in (4, 6): route_diff = self.routes[ip_version].diff() diff --git a/asr1k_neutron_l3/models/neutron/l3/vrf.py b/asr1k_neutron_l3/models/neutron/l3/vrf.py index d28aa3fc..3c19928f 100644 --- a/asr1k_neutron_l3/models/neutron/l3/vrf.py +++ b/asr1k_neutron_l3/models/neutron/l3/vrf.py @@ -34,6 +34,7 @@ def __init__(self, name, description=None, asn=None, rd=None, routable_interface self.asn = asn self.rd = utils.to_rd(self.asn, rd) + # FIXME: hmmm might also need to be AF aware, but... when do we have a routable interface? if self.routable_interface: self.map = f"{cfg.CONF.asr1k_l3.dapnet_rm_prefix}{global_vrf_id:02d}" else: @@ -45,7 +46,7 @@ def __init__(self, name, description=None, asn=None, rd=None, routable_interface self.map_v6 = None self.enable_ipv6 = enable_ipv6 if enable_ipv6: - self.map_v6 = f"exp-v6-{self.name}" + self.map_v6 = f"bgp-redistribute6-{self.name}" self._rest_definition = vrf.VrfDefinition(name=self.name, description=self.description, rd=self.rd, map=self.map, map_v6=self.map_v6, diff --git a/asr1k_neutron_l3/tests/unit/models/netconf_yang/test_parsing.py b/asr1k_neutron_l3/tests/unit/models/netconf_yang/test_parsing.py index 293fb3b3..5a04c010 100644 --- a/asr1k_neutron_l3/tests/unit/models/netconf_yang/test_parsing.py +++ b/asr1k_neutron_l3/tests/unit/models/netconf_yang/test_parsing.py @@ -218,6 +218,95 @@ def test_bgp_parsing(self): parsed_netmasks.add((network['number'], network['mask'])) self.assertEqual(orig_netmasks, parsed_netmasks) + def test_bgp_redistribute_parsing(self): + xml_v4_redist_with_rm = """ + + + + + + 65148 + + + + unicast + + seagull-vrf + + + + test123 + + + + test456 + + + + + + + + + + + + + + """ + + xml_v6_redist_with_rm = """ + + + + + + 65148 + + + + unicast + + seagull-vrf + + + + test123 + + + test456 + + + + + + + + + + + + + """ + context = FakeASR1KContext() + bgp_af4 = bgp.AddressFamilyV4.from_xml(xml_v4_redist_with_rm, context) + self.assertEqual(bgp_af4.redistribute_connected_with_rm, "test123") + self.assertEqual(bgp_af4.redistribute_static_with_rm, "test456") + + bgp_af6 = bgp.AddressFamilyV6.from_xml(xml_v6_redist_with_rm, context) + self.assertEqual(bgp_af6.redistribute_connected_with_rm, "test123") + self.assertEqual(bgp_af6.redistribute_static_with_rm, "test456") + + # back to xml + bgp_af4_dict = bgp_af4.to_dict(context) + self.assertEqual("test123", bgp_af4_dict['vrf']['ipv4-unicast']['redistribute-vrf']['connected']['route-map']) + self.assertEqual("test456", + bgp_af4_dict['vrf']['ipv4-unicast']['redistribute-vrf']['static']['default']['route-map']) + + bgp_af6_dict = bgp_af6.to_dict(context) + self.assertEqual("test123", bgp_af6_dict['vrf']['ipv6-unicast']['redistribute-v6']['connected']['route-map']) + self.assertEqual("test456", bgp_af6_dict['vrf']['ipv6-unicast']['redistribute-v6']['static']['route-map']) + def test_static_nat_parsing(self): xml = """