diff --git a/asr1k_neutron_l3/common/config.py b/asr1k_neutron_l3/common/config.py index b6ffdc0e..ca13adf5 100644 --- a/asr1k_neutron_l3/common/config.py +++ b/asr1k_neutron_l3/common/config.py @@ -83,6 +83,11 @@ help="Route-Map to apply to all DAPNet BGP network statements"), cfg.StrOpt('dapnet_extra_routes_rm', default='RM-DAP-EXTRA-ROUTES', help="Route-Map to apply to all BGP network statements for extra routes that are contained in a DAPNet"), + # FIXME: move defaults away from here + cfg.ListOpt('dapn_routable_nets_communities', default=["65126"], + help="Communities to assign to DAPNets (via redistribute statement)"), + cfg.ListOpt('dapn_extra_routes_communities', default=["65126", "4268097541"], + help="Communities to assign to DAPNet extraroutes (via redistribute statement)"), cfg.IntOpt('external_iface_arp_timeout', default=1800, help="Set ARP timeout for the external interface of a router. Set to 0 to not set this attribute"), cfg.IntOpt('internal_iface_arp_timeout', default=0, diff --git a/asr1k_neutron_l3/models/netconf_yang/bgp.py b/asr1k_neutron_l3/models/netconf_yang/bgp.py index 7ed4705b..c4ac523d 100644 --- a/asr1k_neutron_l3/models/netconf_yang/bgp.py +++ b/asr1k_neutron_l3/models/netconf_yang/bgp.py @@ -38,6 +38,7 @@ class BGPConstants(object): REDISTRIBUTE_VRF = "redistribute-vrf" CONNECTED = "connected" STATIC = "static" + DEFAULT = "default" UNICAST = "unicast" NETWORK = "network" WITH_MASK = "with-mask" @@ -82,14 +83,14 @@ def __parameters__(cls): return [ {'key': 'asn', 'id': True, 'yang-key': 'id'}, {'key': 'vrf', 'yang-key': 'name'}, - {'key': 'connected', 'yang-path': 'ipv4-unicast/redistribute', 'default': False, - 'yang-type': YANG_TYPE.EMPTY}, - {'key': 'static', 'yang-path': 'ipv4-unicast/redistribute', 'default': False, - 'yang-type': YANG_TYPE.EMPTY}, {'key': 'connected', 'yang-path': 'ipv4-unicast/redistribute-vrf', 'default': False, 'yang-type': YANG_TYPE.EMPTY}, + {'key': 'connected_with_rm', 'yang-key': 'route-map', + 'yang-path': 'ipv4-unicast/redistribute-vrf/connected'}, {'key': 'static', 'yang-path': 'ipv4-unicast/redistribute-vrf', 'default': False, 'yang-type': YANG_TYPE.EMPTY}, + {'key': 'static_with_rm', 'yang-key': 'route-map', + 'yang-path': 'ipv4-unicast/redistribute-vrf/default/static'}, {'key': 'networks_v4', 'yang-path': 'ipv4-unicast/network', 'yang-key': BGPConstants.WITH_MASK, 'type': [Network], 'default': []}, ] @@ -152,34 +153,37 @@ def __init__(self, **kwargs): self.asn = kwargs.get("asn", None) def to_dict(self, context): - result = OrderedDict() - if self.vrf is not None: - vrf = OrderedDict() - vrf[BGPConstants.NAME] = self.vrf - vrf[BGPConstants.IPV4_UNICAST] = { - xml_utils.OPERATION: NC_OPERATION.PUT, - } + if self.vrf is None: + return {} - REDIST_CONST = BGPConstants.REDISTRIBUTE_VRF if context.version_min_17_3 else BGPConstants.REDISTRIBUTE - - # redistribute connected/static is only used with 16.9 - if self.from_device or not context.version_min_17_3: - if self.connected or self.static: - vrf[BGPConstants.IPV4_UNICAST][REDIST_CONST] = {} - if self.connected: - vrf[BGPConstants.IPV4_UNICAST][REDIST_CONST][BGPConstants.CONNECTED] = '' - if self.static: - vrf[BGPConstants.IPV4_UNICAST][REDIST_CONST][BGPConstants.STATIC] = '' - - # networks are currently only announced with 17.4 - if self.networks_v4 and (self.from_device or context.version_min_17_3): - vrf[BGPConstants.IPV4_UNICAST][BGPConstants.NETWORK] = { - BGPConstants.WITH_MASK: [ - net.to_dict(context) for net in sorted(self.networks_v4, key=lambda x: (x.number, x.mask)) - ] - } - result[BGPConstants.VRF] = vrf - return dict(result) + result = {} + + vrf = {} + vrf[BGPConstants.NAME] = self.vrf + vrf[BGPConstants.IPV4_UNICAST] = { + xml_utils.OPERATION: NC_OPERATION.PUT, + } + + if self.connected or self.connected_with_rm or self.static or self.static_with_rm: + redist = vrf[BGPConstants.IPV4_UNICAST].setdefault(BGPConstants.REDISTRIBUTE_VRF, {}) + if self.connected_with_rm: + redist[BGPConstants.CONNECTED] = {BGPConstants.ROUTE_MAP: self.connected_with_rm} + elif self.connected: + redist[BGPConstants.CONNECTED] = '' + + if self.static_with_rm: + redist[BGPConstants.STATIC] = {BGPConstants.DEFAULT: {BGPConstants.ROUTE_MAP: self.static_with_rm}} + elif self.static: + redist[BGPConstants.STATIC] = '' + + if self.networks_v4: + vrf[BGPConstants.IPV4_UNICAST][BGPConstants.NETWORK] = { + BGPConstants.WITH_MASK: [ + net.to_dict(context) for net in sorted(self.networks_v4, key=lambda x: (x.number, x.mask)) + ] + } + result[BGPConstants.VRF] = vrf + return result def to_delete_dict(self, context): result = OrderedDict() diff --git a/asr1k_neutron_l3/models/netconf_yang/prefix.py b/asr1k_neutron_l3/models/netconf_yang/prefix.py index 6bd9381f..a770a83f 100644 --- a/asr1k_neutron_l3/models/netconf_yang/prefix.py +++ b/asr1k_neutron_l3/models/netconf_yang/prefix.py @@ -92,12 +92,10 @@ def __init__(self, **kwargs): @property def neutron_router_id(self): - if self.name is not None and self.name.startswith('ext-'): - return utils.vrf_id_to_uuid(self.name[4:]) - elif self.name is not None and self.name.startswith('snat-'): - return utils.vrf_id_to_uuid(self.name[5:]) - elif self.name is not None and self.name.startswith('route-'): - return utils.vrf_id_to_uuid(self.name[6:]) + for prefix in 'ext-', 'snat-', 'route-', 'BGPVPN-', 'ROUTABLEINT-', 'ROUTABLEEXTRA-': + if self.name and self.name.startswith(prefix): + return utils.vrf_id_to_uuid(self.name[len(prefix):]) + return None def add_seq(self, seq): if seq.no is None: diff --git a/asr1k_neutron_l3/models/netconf_yang/route_map.py b/asr1k_neutron_l3/models/netconf_yang/route_map.py index 2f520347..24fc26ca 100644 --- a/asr1k_neutron_l3/models/netconf_yang/route_map.py +++ b/asr1k_neutron_l3/models/netconf_yang/route_map.py @@ -29,6 +29,9 @@ class RouteMapConstants(object): OPERATION = "operation" SET = "set" EXTCOMMUNITY = 'extcommunity' + COMMUNITY = 'community' + COMMUNITY_WELL_KNOWN = 'community-well-known' + COMMUNITY_LIST = 'community-list' RT = 'rt' RANGE = 'range' ADDITIVE = 'additive' @@ -128,6 +131,8 @@ def __parameters__(cls): {'key': 'prefix_list', 'yang-key': 'prefix-list', 'yang-path': 'match/ip/address'}, {'key': 'access_list', 'yang-key': 'access-list', 'yang-path': 'match/ip/address'}, {'key': 'ip_precedence', 'yang-path': 'set/ip/precedence', 'yang-key': 'precedence-fields'}, + {'key': 'community_list', 'yang-key': 'community-list', 'yang-path': 'set/community/community-well-known', + 'default': []}, {'key': 'drop_on_17_3', 'default': False}, ] @@ -158,24 +163,26 @@ def to_dict(self, context): seq[RouteMapConstants.OPERATION] = self.operation if self.asn: - seq[RouteMapConstants.SET] = { - RouteMapConstants.EXTCOMMUNITY: {RouteMapConstants.RT: {RouteMapConstants.ASN: self.asn}}} + seq.setdefault(RouteMapConstants.SET, {}) + seq[RouteMapConstants.SET][RouteMapConstants.EXTCOMMUNITY] = { + RouteMapConstants.RT: {RouteMapConstants.ASN: self.asn}, + } if self.next_hop is not None: + seq.setdefault(RouteMapConstants.SET, {}) if context.version_min_17_3: - seq[RouteMapConstants.SET] = { - RouteMapConstants.IP: {RouteMapConstants.NEXT_HOP: {RouteMapConstants.ADDRESS: [self.next_hop]}} + seq[RouteMapConstants.SET][RouteMapConstants.IP] = { + RouteMapConstants.NEXT_HOP: {RouteMapConstants.ADDRESS: [self.next_hop]}, } if self.force: # it looks like the force flag is now part of the address list in 17.3+ seq[RouteMapConstants.SET][RouteMapConstants.IP][RouteMapConstants.NEXT_HOP][ RouteMapConstants.ADDRESS].append(RouteMapConstants.FORCE) else: - seq[RouteMapConstants.SET] = { - RouteMapConstants.IP: { - RouteMapConstants.NEXT_HOP: { - RouteMapConstants.NEXT_HOP_ADDR: { - RouteMapConstants.ADDRESS: self.next_hop}}}} + seq[RouteMapConstants.SET][RouteMapConstants.IP] = { + RouteMapConstants.NEXT_HOP: { + RouteMapConstants.NEXT_HOP_ADDR: { + RouteMapConstants.ADDRESS: self.next_hop}}} if self.force: seq[RouteMapConstants.SET][RouteMapConstants.IP][RouteMapConstants.NEXT_HOP][ RouteMapConstants.NEXT_HOP_ADDR][RouteMapConstants.FORCE] = "" @@ -188,13 +195,19 @@ def to_dict(self, context): seq[RouteMapConstants.MATCH] = { RouteMapConstants.IP: {RouteMapConstants.ADDRESS: {RouteMapConstants.ACCESS_LIST: self.access_list}}} if self.ip_precedence: - if RouteMapConstants.SET not in seq: - seq[RouteMapConstants.SET] = {} + seq.setdefault(RouteMapConstants.SET, {}) if RouteMapConstants.IP not in seq[RouteMapConstants.SET]: seq[RouteMapConstants.SET][RouteMapConstants.IP] = {} seq[RouteMapConstants.SET][RouteMapConstants.IP][RouteMapConstants.PRECEDENCE] = { RouteMapConstants.PRECEDENCE_FIELDS: self.ip_precedence, } + if self.community_list: + seq.setdefault(RouteMapConstants.SET, {}) + seq[RouteMapConstants.SET][RouteMapConstants.COMMUNITY] = { + RouteMapConstants.COMMUNITY_WELL_KNOWN: { + RouteMapConstants.COMMUNITY_LIST: self.community_list, + }, + } seq[xml_utils.NS] = xml_utils.NS_CISCO_ROUTE_MAP return seq diff --git a/asr1k_neutron_l3/models/neutron/l3/bgp.py b/asr1k_neutron_l3/models/neutron/l3/bgp.py index 8521320a..d309157f 100644 --- a/asr1k_neutron_l3/models/neutron/l3/bgp.py +++ b/asr1k_neutron_l3/models/neutron/l3/bgp.py @@ -23,7 +23,8 @@ class AddressFamily(base.Base): def __init__(self, vrf, asn=None, routable_interface=False, rt_export=[], - connected_cidrs=[], routable_networks=[], extra_routes=[]): + connected_cidrs=[], routable_networks=[], extra_routes=[], + redist_rm=None): super(AddressFamily, self).__init__() self.vrf = utils.uuid_to_vrf_id(vrf) self.routable_interface = routable_interface @@ -50,7 +51,8 @@ def __init__(self, vrf, asn=None, routable_interface=False, rt_export=[], self.enable_bgp = True self._rest_definition = bgp.AddressFamily(vrf=self.vrf, asn=self.asn, enable_bgp=self.enable_bgp, - static=True, connected=True, networks_v4=self.networks_v4) + networks_v4=self.networks_v4, + static_with_rm=redist_rm, connected_with_rm=redist_rm) def get(self): return bgp.AddressFamily.get(self.vrf, asn=self.asn, enable_bgp=self.enable_bgp) diff --git a/asr1k_neutron_l3/models/neutron/l3/prefix.py b/asr1k_neutron_l3/models/neutron/l3/prefix.py index 9d887a5e..27a888dd 100644 --- a/asr1k_neutron_l3/models/neutron/l3/prefix.py +++ b/asr1k_neutron_l3/models/neutron/l3/prefix.py @@ -24,6 +24,7 @@ class BasePrefix(base.Base): def __init__(self, name_prefix, router_id, gateway_interface, internal_interfaces): self.vrf = utils.uuid_to_vrf_id(router_id) + self.list_name = f"{name_prefix}-{self.vrf}" self.internal_interfaces = internal_interfaces self.gateway_interface = gateway_interface self.gateway_address_scope = None @@ -32,7 +33,7 @@ def __init__(self, name_prefix, router_id, gateway_interface, internal_interface if self.gateway_interface is not None: self.gateway_address_scope = self.gateway_interface.address_scope - self._rest_definition = prefix.Prefix(name="{}-{}".format(name_prefix, self.vrf)) + self._rest_definition = prefix.Prefix(name=self.list_name) class ExtPrefix(BasePrefix): @@ -74,3 +75,26 @@ def __init__(self, router_id=None, gateway_interface=None, internal_interfaces=N permit_ge = utils.prefix_from_cidr(cidr) + 1 self._rest_definition.add_seq(prefix.PrefixSeq(no=i * 10, permit_ip=cidr, permit_ge=permit_ge)) i += 1 + + +class BasePrefixWithPrefixes(BasePrefix): + PREFIX_NAME = None + + def __init__(self, router_id, prefixes): + super().__init__(self.PREFIX_NAME, router_id, None, None) + + self.has_prefixes = bool(prefixes) + for n, cidr in enumerate(sorted(prefixes), 1): + self._rest_definition.add_seq(prefix.PrefixSeq(no=n * 10, permit_ip=cidr)) + + +class BgpvpnPrefixes(BasePrefixWithPrefixes): + PREFIX_NAME = "BGPVPN" + + +class RoutableInternalPrefixes(BasePrefixWithPrefixes): + PREFIX_NAME = "ROUTABLEINT" + + +class RoutableExtraPrefixes(BasePrefixWithPrefixes): + PREFIX_NAME = "ROUTABLEEXTRA" diff --git a/asr1k_neutron_l3/models/neutron/l3/route_map.py b/asr1k_neutron_l3/models/neutron/l3/route_map.py index 45e8bf8a..5d368321 100644 --- a/asr1k_neutron_l3/models/neutron/l3/route_map.py +++ b/asr1k_neutron_l3/models/neutron/l3/route_map.py @@ -13,9 +13,11 @@ # 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 oslo_config import cfg + from asr1k_neutron_l3.common import utils from asr1k_neutron_l3.models.netconf_yang import route_map -from asr1k_neutron_l3.models.neutron.l3 import base +from asr1k_neutron_l3.models.neutron.l3 import base, prefix class RouteMap(base.Base): @@ -82,3 +84,27 @@ def __init__(self, name, gateway_interface=None): ip_precedence='routine')) self._rest_definition = route_map.RouteMap(name=self.name, seq=sequences) + + +class BgpvpnRedistRouteMap(base.Base): + def __init__(self, router_id): + super().__init__() + + self.vrf = utils.uuid_to_vrf_id(router_id) + self.name = "BGPVPNREDIST-{}".format(self.vrf) + + sequences = [ + route_map.MapSequence(seq_no=10, + operation='permit', + community_list=cfg.CONF.asr1k_l3.dapn_routable_nets_communities, + access_list=f"{prefix.RoutableInternalPrefixes.PREFIX_NAME}-{self.vrf}"), + route_map.MapSequence(seq_no=20, + operation='permit', + community_list=cfg.CONF.asr1k_l3.dapn_extra_routes_communities, + access_list=f"{prefix.RoutableExtraPrefixes.PREFIX_NAME}-{self.vrf}"), + route_map.MapSequence(seq_no=30, + operation='permit', + access_list=f"{prefix.BgpvpnPrefixes.PREFIX_NAME}-{self.vrf}"), + ] + + self._rest_definition = route_map.RouteMap(name=self.name, seq=sequences) diff --git a/asr1k_neutron_l3/models/neutron/l3/router.py b/asr1k_neutron_l3/models/neutron/l3/router.py index 603f0296..bf50c322 100644 --- a/asr1k_neutron_l3/models/neutron/l3/router.py +++ b/asr1k_neutron_l3/models/neutron/l3/router.py @@ -94,6 +94,7 @@ def __init__(self, router_info): 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.bgpvpn_extras = self._build_bgpvpn_extras() self.dynamic_nat = self._build_dynamic_nat() self.nat_pool = self._build_nat_pool() @@ -222,16 +223,37 @@ def _route_has_connected_interface(self, l3_route): return False def _build_bgp_address_family(self): - connected_cidrs = self.get_internal_cidrs() - extra_routes = list() - if self.router_info["bgpvpn_advertise_extra_routes"]: - extra_routes = [x.cidr for x in self.routes.routes if x.cidr != "0.0.0.0/0"] - return bgp.AddressFamily(self.router_info.get('id'), asn=self.config.asr1k_l3.fabric_asn, routable_interface=self.routable_interface, - rt_export=self.rt_export, connected_cidrs=connected_cidrs, + rt_export=self.rt_export, connected_cidrs=[], routable_networks=self.get_routable_networks(), - extra_routes=extra_routes) + extra_routes=[], + redist_rm="BGPVPNREDIST-{}".format(utils.uuid_to_vrf_id(self.router_id))) + + def _build_bgpvpn_extras(self): + extra_routes = [] + if self.router_info["bgpvpn_advertise_extra_routes"]: + extra_routes = [x.cidr for x in self.routes.routes if x.cidr != "0.0.0.0/0"] + + routable_internal = [] + routable_extra = [] + bgpvpn_cidrs = [] + for cidr in self.get_internal_cidrs() + extra_routes: + if any(utils.network_in_network(cidr, rn) for rn in self.get_routable_networks()): + if cidr in extra_routes: + routable_extra.append(cidr) + else: + routable_internal.append(cidr) + else: + bgpvpn_cidrs.append(cidr) + + return [ + prefix.RoutableInternalPrefixes(self.router_id, routable_internal), + prefix.RoutableExtraPrefixes(self.router_id, routable_extra), + prefix.BgpvpnPrefixes(self.router_id, bgpvpn_cidrs), + + route_map.BgpvpnRedistRouteMap(self.router_id), + ] def _build_dynamic_nat(self): pool_nat = nat.DynamicNAT(self.router_id, gateway_interface=self.gateway_interface, @@ -347,8 +369,10 @@ def _update(self): if self.routable_interface or len(self.rt_export) > 0: results.append(self.bgp_address_family.update()) + results.extend(obj.update() for obj in self.bgpvpn_extras) else: results.append(self.bgp_address_family.delete()) + results.extend(obj.delete() for obj in self.bgpvpn_extras) if self.nat_acl: results.append(self.nat_acl.update()) @@ -416,6 +440,7 @@ def _delete(self): results.append(self.nat_acl.delete()) results.append(self.pbr_acl.delete()) results.append(self.bgp_address_family.delete()) + results.extend(obj.delete() for obj in self.bgpvpn_extras) for interface in self.interfaces.all_interfaces: results.append(interface.delete()) @@ -437,6 +462,11 @@ def diff(self): if not bgp_diff.valid: diff_results['bgp'] = bgp_diff.to_dict() + for obj in self.bgpvpn_extras: + obj_diff = obj.diff() + if not obj_diff.valid: + diff_results.setdefault('bgpvpn_extras', []).append(obj_diff.to_dict()) + for prefix_list in self.prefix_lists: if prefix_list.internal_interfaces is not None and len(prefix_list.internal_interfaces) > 0: prefix_diff = prefix_list.diff() 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 a6788793..4f819564 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 @@ -20,6 +20,7 @@ from asr1k_neutron_l3.models.netconf_yang.l2_interface import BridgeDomain from asr1k_neutron_l3.models.netconf_yang.vrf import VrfDefinition from asr1k_neutron_l3.models.netconf_yang.nat import StaticNatList +from asr1k_neutron_l3.models.netconf_yang.route_map import RouteMap class ParsingTest(base.BaseTestCase): @@ -246,7 +247,9 @@ def test_bgp_parsing(self): - + + stonechat + @@ -266,6 +269,10 @@ def test_bgp_parsing(self): context = FakeASR1KContext() bgp_af = bgp.AddressFamily.from_xml(bgp_xml, context) + self.assertTrue(bgp_af.connected) + self.assertEqual("stonechat", bgp_af.connected_with_rm) + self.assertTrue(bgp_af.static) + self.assertIsNone(bgp_af.static_with_rm) parsed_cidrs = {net.cidr for net in bgp_af.networks_v4} self.assertEqual(orig_cidrs, parsed_cidrs) for network in bgp_af.networks_v4: @@ -408,3 +415,33 @@ 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_parse_route_map_community_set(self): + xml = """ + + + + + RM-DAP-EXTRA-ROUTES + + 10 + permit + + + + 65126 + 4268097541 + + + + + + + + +""" + context = FakeASR1KContext() + rm = RouteMap.from_xml(xml, context) + + self.assertEqual(["65126", "4268097541"], rm.seq[0].community_list)