1 # Copyright (C) 2014 Nippon Telegraph and Telephone Corporation.
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
17 Defines base data types and models required specifically for VRF support.
24 from ryu.lib.packet.bgp import BGP_ATTR_TYPE_ORIGIN
25 from ryu.lib.packet.bgp import BGP_ATTR_TYPE_AS_PATH
26 from ryu.lib.packet.bgp import BGP_ATTR_TYPE_EXTENDED_COMMUNITIES
27 from ryu.lib.packet.bgp import BGP_ATTR_TYEP_PMSI_TUNNEL_ATTRIBUTE
28 from ryu.lib.packet.bgp import BGP_ATTR_TYPE_MULTI_EXIT_DISC
29 from ryu.lib.packet.bgp import BGPPathAttributeOrigin
30 from ryu.lib.packet.bgp import BGPPathAttributeAsPath
31 from ryu.lib.packet.bgp import EvpnEthernetSegmentNLRI
32 from ryu.lib.packet.bgp import BGPPathAttributeExtendedCommunities
33 from ryu.lib.packet.bgp import BGPPathAttributeMultiExitDisc
34 from ryu.lib.packet.bgp import BGPEncapsulationExtendedCommunity
35 from ryu.lib.packet.bgp import BGPEvpnEsiLabelExtendedCommunity
36 from ryu.lib.packet.bgp import BGPEvpnEsImportRTExtendedCommunity
37 from ryu.lib.packet.bgp import BGPPathAttributePmsiTunnel
38 from ryu.lib.packet.bgp import PmsiTunnelIdIngressReplication
39 from ryu.lib.packet.bgp import RF_L2_EVPN
40 from ryu.lib.packet.bgp import EvpnMacIPAdvertisementNLRI
41 from ryu.lib.packet.bgp import EvpnIpPrefixNLRI
42 from ryu.lib.packet.safi import (
47 from ryu.services.protocols.bgp.base import OrderedDict
48 from ryu.services.protocols.bgp.constants import VPN_TABLE
49 from ryu.services.protocols.bgp.constants import VRF_TABLE
50 from ryu.services.protocols.bgp.info_base.base import Destination
51 from ryu.services.protocols.bgp.info_base.base import Path
52 from ryu.services.protocols.bgp.info_base.base import Table
53 from ryu.services.protocols.bgp.utils.bgp import create_rt_extended_community
54 from ryu.services.protocols.bgp.utils.stats import LOCAL_ROUTES
55 from ryu.services.protocols.bgp.utils.stats import REMOTE_ROUTES
56 from ryu.services.protocols.bgp.utils.stats import RESOURCE_ID
57 from ryu.services.protocols.bgp.utils.stats import RESOURCE_NAME
59 LOG = logging.getLogger('bgpspeaker.info_base.vrf')
62 @six.add_metaclass(abc.ABCMeta)
63 class VrfTable(Table):
64 """Virtual Routing and Forwarding information base.
65 Keeps destination imported to given vrf in represents.
69 VPN_ROUTE_FAMILY = None
74 def __init__(self, vrf_conf, core_service, signal_bus):
75 Table.__init__(self, vrf_conf.route_dist, core_service, signal_bus)
76 self._vrf_conf = vrf_conf
77 self._import_maps = []
78 self.init_import_maps(vrf_conf.import_maps)
80 def init_import_maps(self, import_maps):
82 "Initializing import maps (%s) for %r", import_maps, self
84 del self._import_maps[:]
85 importmap_manager = self._core_service.importmap_manager
86 for name in import_maps:
87 import_map = importmap_manager.get_import_map_by_name(name)
88 if import_map is None:
89 raise KeyError('No import map with name %s' % name)
90 self._import_maps.append(import_map)
94 return self._vrf_conf.import_rts
100 def _table_key(self, nlri):
101 """Return a key that will uniquely identify this NLRI inside
104 # Note: We use `prefix` representation of the NLRI, because
105 # BGP route can be identified without the route distinguisher
106 # value in the VRF space.
109 def _create_dest(self, nlri):
110 return self.VRF_DEST_CLASS(self, nlri)
112 def append_import_map(self, import_map):
113 self._import_maps.append(import_map)
115 def remove_import_map(self, import_map):
116 self._import_maps.remove(import_map)
118 def get_stats_summary_dict(self):
119 """Returns count of local and remote paths."""
121 remote_route_count = 0
122 local_route_count = 0
123 for dest in self.values():
124 for path in dest.known_path_list:
125 if (hasattr(path.source, 'version_num') or
126 path.source == VPN_TABLE):
127 remote_route_count += 1
129 local_route_count += 1
130 return {RESOURCE_ID: self._vrf_conf.id,
131 RESOURCE_NAME: self._vrf_conf.name,
132 REMOTE_ROUTES: remote_route_count,
133 LOCAL_ROUTES: local_route_count}
135 def import_vpn_paths_from_table(self, vpn_table, import_rts=None):
136 for vpn_dest in vpn_table.values():
137 vpn_path = vpn_dest.best_path
141 if import_rts is None:
142 import_rts = set(self.import_rts)
144 import_rts = set(import_rts)
146 path_rts = vpn_path.get_rts()
147 if import_rts.intersection(path_rts):
148 # TODO(PH): When (re-)implementing extranet, check what should
149 # be the label reported back to NC for local paths coming from
151 self.import_vpn_path(vpn_path)
153 def import_vpn_path(self, vpn_path):
154 """Imports `vpnv(4|6)_path` into `vrf(4|6)_table` or `evpn_path`
158 - `vpn_path`: (Path) VPN path that will be cloned and imported
160 Note: Does not do any checking if this import is valid.
162 assert vpn_path.route_family == self.VPN_ROUTE_FAMILY
163 # If source of given vpnv4 path is NC we import it to given VRF
164 # table because of extranet setting. Hence we identify source of
165 # EXTRANET prefixes as VRF_TABLE, else VPN_TABLE.
166 source = vpn_path.source
170 if self.VPN_ROUTE_FAMILY == RF_L2_EVPN:
171 # Because NLRI class is the same if the route family is EVPN,
172 # we re-use the NLRI instance.
173 vrf_nlri = vpn_path.nlri
174 elif self.ROUTE_FAMILY.safi in [IP_FLOWSPEC, VPN_FLOWSPEC]:
175 vrf_nlri = self.NLRI_CLASS(rules=vpn_path.nlri.rules)
176 else: # self.VPN_ROUTE_FAMILY in [RF_IPv4_VPN, RF_IPv6_VPN]
178 ip, masklen = vpn_path.nlri.prefix.split('/')
179 vrf_nlri = self.NLRI_CLASS(length=int(masklen), addr=ip)
181 vrf_path = self.VRF_PATH_CLASS(
182 puid=self.VRF_PATH_CLASS.create_puid(
183 vpn_path.nlri.route_dist,
184 vpn_path.nlri.prefix),
187 src_ver_num=vpn_path.source_version_num,
188 pattrs=vpn_path.pathattr_map,
189 nexthop=vpn_path.nexthop,
190 is_withdraw=vpn_path.is_withdraw,
191 label_list=getattr(vpn_path.nlri, 'label_list', None),
193 if self._is_vrf_path_already_in_table(vrf_path):
196 if self._is_vrf_path_filtered_out_by_import_maps(vrf_path):
199 vrf_dest = self.insert(vrf_path)
200 self._signal_bus.dest_changed(vrf_dest)
202 def _is_vrf_path_filtered_out_by_import_maps(self, vrf_path):
203 for import_map in self._import_maps:
204 if import_map.match(vrf_path):
209 def _is_vrf_path_already_in_table(self, vrf_path):
210 dest = self._get_dest(vrf_path.nlri)
213 return vrf_path in dest.known_path_list
215 def apply_import_maps(self):
217 for dest in self.values():
218 assert isinstance(dest, VrfDest)
219 for import_map in self._import_maps:
220 for path in dest.known_path_list:
221 if import_map.match(path):
222 dest.withdraw_path(path)
223 changed_dests.append(dest)
226 def insert_vrf_path(self, nlri, next_hop=None,
227 gen_lbl=False, is_withdraw=False, **kwargs):
231 vrf_conf = self.vrf_conf
233 table_manager = self._core_service.table_manager
234 if gen_lbl and next_hop:
235 # Label per next_hop demands we use a different label
236 # per next_hop. Here connected interfaces are advertised per
238 label_key = (vrf_conf.route_dist, next_hop)
239 nh_label = table_manager.get_nexthop_label(label_key)
241 nh_label = table_manager.get_next_vpnv4_label()
242 table_manager.set_nexthop_label(label_key, nh_label)
243 label_list.append(nh_label)
246 # If we do not have next_hop, get a new label.
247 label_list.append(table_manager.get_next_vpnv4_label())
249 # Set MPLS labels with the generated labels
250 if gen_lbl and isinstance(nlri, EvpnMacIPAdvertisementNLRI):
251 nlri.mpls_labels = label_list[:2]
252 elif gen_lbl and isinstance(nlri, EvpnIpPrefixNLRI):
253 nlri.mpls_label = label_list[0]
255 # Create a dictionary for path-attrs.
256 pattrs = OrderedDict()
258 # MpReachNlri and/or MpUnReachNlri attribute info. is contained
259 # in the path. Hence we do not add these attributes here.
260 from ryu.services.protocols.bgp.core import EXPECTED_ORIGIN
262 pattrs[BGP_ATTR_TYPE_ORIGIN] = BGPPathAttributeOrigin(
264 pattrs[BGP_ATTR_TYPE_AS_PATH] = BGPPathAttributeAsPath([])
267 # Set ES-Import Route Target
268 if isinstance(nlri, EvpnEthernetSegmentNLRI):
270 es_import = nlri.esi.mac_addr
271 communities.append(BGPEvpnEsImportRTExtendedCommunity(
273 es_import=es_import))
275 for rt in vrf_conf.export_rts:
276 communities.append(create_rt_extended_community(rt, 2))
277 for soo in vrf_conf.soo_list:
278 communities.append(create_rt_extended_community(soo, 3))
280 # Set Tunnel Encapsulation Attribute
281 tunnel_type = kwargs.get('tunnel_type', None)
284 BGPEncapsulationExtendedCommunity.from_str(tunnel_type))
286 # Set ESI Label Extended Community
287 redundancy_mode = kwargs.get('redundancy_mode', None)
288 if redundancy_mode is not None:
292 from ryu.services.protocols.bgp.api.prefix import (
293 REDUNDANCY_MODE_SINGLE_ACTIVE)
294 if redundancy_mode == REDUNDANCY_MODE_SINGLE_ACTIVE:
295 flags |= BGPEvpnEsiLabelExtendedCommunity.SINGLE_ACTIVE_BIT
297 vni = kwargs.get('vni', None)
299 communities.append(BGPEvpnEsiLabelExtendedCommunity(
304 communities.append(BGPEvpnEsiLabelExtendedCommunity(
307 mpls_label=label_list[0]))
309 pattrs[BGP_ATTR_TYPE_EXTENDED_COMMUNITIES] = \
310 BGPPathAttributeExtendedCommunities(communities=communities)
311 if vrf_conf.multi_exit_disc:
312 pattrs[BGP_ATTR_TYPE_MULTI_EXIT_DISC] = \
313 BGPPathAttributeMultiExitDisc(vrf_conf.multi_exit_disc)
315 # Set PMSI Tunnel Attribute
316 pmsi_tunnel_type = kwargs.get('pmsi_tunnel_type', None)
317 if pmsi_tunnel_type is not None:
318 from ryu.services.protocols.bgp.api.prefix import (
319 PMSI_TYPE_INGRESS_REP)
320 if pmsi_tunnel_type == PMSI_TYPE_INGRESS_REP:
321 tunnel_id = PmsiTunnelIdIngressReplication(
322 tunnel_endpoint_ip=self._core_service.router_id)
323 else: # pmsi_tunnel_type == PMSI_TYPE_NO_TUNNEL_INFO
325 pattrs[BGP_ATTR_TYEP_PMSI_TUNNEL_ATTRIBUTE] = \
326 BGPPathAttributePmsiTunnel(pmsi_flags=0,
327 tunnel_type=pmsi_tunnel_type,
329 vni=kwargs.get('vni', None))
331 puid = self.VRF_PATH_CLASS.create_puid(
332 vrf_conf.route_dist, nlri.prefix)
334 path = self.VRF_PATH_CLASS(
335 puid, None, nlri, 0, pattrs=pattrs,
336 nexthop=next_hop, label_list=label_list,
337 is_withdraw=is_withdraw
340 # Insert the path into VRF table, get affected destination so that we
341 # can process it further.
342 eff_dest = self.insert(path)
343 # Enqueue the eff_dest for further processing.
344 self._signal_bus.dest_changed(eff_dest)
347 def clean_uninteresting_paths(self, interested_rts=None):
348 if interested_rts is None:
349 interested_rts = set(self.vrf_conf.import_rts)
350 return super(VrfTable, self).clean_uninteresting_paths(interested_rts)
353 @six.add_metaclass(abc.ABCMeta)
354 class VrfDest(Destination):
355 """Base class for VRF destination."""
357 def __init__(self, table, nlri):
358 super(VrfDest, self).__init__(table, nlri)
359 self._route_dist = self._table.vrf_conf.route_dist
363 # Returns `prefix` without the route distinguisher value, because
364 # a destination in VRF space can be identified without the route
366 return self._nlri.prefix
368 def _best_path_lost(self):
369 # Have to send update messages for withdraw of best-path to Network
370 # controller or Global table.
371 old_best_path = self._best_path
372 self._best_path = None
374 if old_best_path is None:
377 if old_best_path.source is not None:
378 # Send update-withdraw msg. to Sink. Create withdraw path
379 # out of old best path and queue it into flexinet sinks.
380 old_best_path = old_best_path.clone(for_withdrawal=True)
381 self._core_service.update_flexinet_peers(old_best_path,
384 # Create withdraw-path out of old best path.
385 gpath = old_best_path.clone_to_vpn(self._route_dist,
387 # Insert withdraw into global table and enqueue the destination
388 # for further processing.
389 tm = self._core_service.table_manager
392 def _new_best_path(self, best_path):
393 LOG.debug('New best path selected for destination %s', self)
395 old_best_path = self._best_path
396 assert (best_path != old_best_path)
397 self._best_path = best_path
398 # Distribute new best-path to flexinet-peers.
399 if best_path.source is not None:
400 # Since route-refresh just causes the version number to
401 # go up and this changes best-path, we check if new-
402 # best-path is really different than old-best-path that
403 # warrants sending update to flexinet peers.
406 old_labels = old_best_path.label_list
407 new_labels = best_path.label_list
408 return old_best_path.nexthop != best_path.nexthop \
409 or set(old_labels) != set(new_labels)
411 if not old_best_path or (old_best_path and really_diff()):
412 # Create OutgoingRoute and queue it into NC sink.
413 self._core_service.update_flexinet_peers(
414 best_path, self._route_dist
417 # If NC is source, we create new path and insert into global
419 gpath = best_path.clone_to_vpn(self._route_dist)
420 tm = self._core_service.table_manager
422 LOG.debug('VRF table %s has new best path: %s',
423 self._route_dist, self.best_path)
425 def _remove_withdrawals(self):
426 """Removes withdrawn paths.
429 We may have disproportionate number of withdraws compared to know paths
430 since not all paths get installed into the table due to bgp policy and
431 we can receive withdraws for such paths and withdrawals may not be
432 stopped by the same policies.
435 LOG.debug('Removing %s withdrawals', len(self._withdraw_list))
437 # If we have not withdrawals, we have nothing to do.
438 if not self._withdraw_list:
441 # If we have some withdrawals and no know-paths, it means it is safe to
442 # delete these withdraws.
443 if not self._known_path_list:
444 LOG.debug('Found %s withdrawals for path(s) that did not get'
445 ' installed.', len(self._withdraw_list))
446 del (self._withdraw_list[:])
449 # If we have some known paths and some withdrawals, we find matches and
453 # Match all withdrawals from destination paths.
454 for withdraw in self._withdraw_list:
456 for path in self._known_path_list:
457 # We have a match if the source are same.
458 if path.puid == withdraw.puid:
461 w_matches.append(withdraw)
462 # One withdraw can remove only one path.
464 # We do no have any match for this withdraw.
466 LOG.debug('No matching path for withdraw found, may be path '
467 'was not installed into table: %s',
469 # If we have partial match.
470 if len(matches) != len(self._withdraw_list):
471 LOG.debug('Did not find match for some withdrawals. Number of '
472 'matches(%s), number of withdrawals (%s)',
473 len(matches), len(self._withdraw_list))
475 # Clear matching paths and withdrawals.
476 for match in matches:
477 self._known_path_list.remove(match)
478 for w_match in w_matches:
479 self._withdraw_list.remove(w_match)
481 def _remove_old_paths(self):
482 """Identifies which of known paths are old and removes them.
484 Known paths will no longer have paths whose new version is present in
487 new_paths = self._new_path_list
488 known_paths = self._known_path_list
489 for new_path in new_paths:
491 for path in known_paths:
492 # Here we just check if source is same and not check if path
493 # version num. as new_paths are implicit withdrawal of old
494 # paths and when doing RouteRefresh (not EnhancedRouteRefresh)
495 # we get same paths again.
496 if new_path.puid == path.puid:
497 old_paths.append(path)
500 for old_path in old_paths:
501 known_paths.remove(old_path)
502 LOG.debug('Implicit withdrawal of old path, since we have'
503 ' learned new path from same source: %s', old_path)
505 def _validate_path(self, path):
506 if not path or not hasattr(path, 'label_list'):
507 raise ValueError('Invalid value of path. Expected type '
508 'with attribute label_list got %s' % path)
511 @six.add_metaclass(abc.ABCMeta)
513 """Represents a way of reaching an IP destination with a VPN.
515 __slots__ = ('_label_list', '_puid')
518 VPN_PATH_CLASS = None
519 VPN_NLRI_CLASS = None
521 def __init__(self, puid, source, nlri, src_ver_num,
522 pattrs=None, nexthop=None,
523 is_withdraw=False, label_list=None):
524 """Initializes a Vrf path.
527 - `puid`: (str) path ID, identifies VPN path from which this
528 VRF path was imported.
529 - `label_list`: (list) List of labels for this path.
530 Note: other parameters are as documented in super class.
532 if self.ROUTE_FAMILY.safi in [IP_FLOWSPEC, VPN_FLOWSPEC]:
535 Path.__init__(self, source, nlri, src_ver_num, pattrs, nexthop,
537 if label_list is None:
539 self._label_list = label_list
548 tokens = self.puid.split(':')
549 return tokens[0] + ':' + tokens[1]
552 def label_list(self):
553 return self._label_list[:]
557 # Returns `prefix` without the route distinguisher value, because
558 # a destination in VRF space can be identified without the route
560 return self._nlri.prefix
563 def create_puid(route_dist, ip_prefix):
564 assert route_dist and ip_prefix
565 return str(route_dist) + ':' + ip_prefix
567 def clone(self, for_withdrawal=False):
569 if not for_withdrawal:
570 pathattrs = self.pathattr_map
572 clone = self.__class__(
576 self.source_version_num,
578 nexthop=self.nexthop,
579 is_withdraw=for_withdrawal,
580 label_list=self.label_list
584 def clone_to_vpn(self, route_dist, for_withdrawal=False):
585 if self.ROUTE_FAMILY == RF_L2_EVPN:
586 # Because NLRI class is the same if the route family is EVPN,
587 # we re-use the NLRI instance.
588 vpn_nlri = self._nlri
590 elif self.ROUTE_FAMILY.safi in [IP_FLOWSPEC, VPN_FLOWSPEC]:
591 vpn_nlri = self.VPN_NLRI_CLASS(route_dist=route_dist,
592 rules=self.nlri.rules)
594 else: # self.ROUTE_FAMILY in [RF_IPv4_UC, RF_IPv6_UC]
595 ip, masklen = self._nlri.prefix.split('/')
596 vpn_nlri = self.VPN_NLRI_CLASS(length=int(masklen),
598 labels=self.label_list,
599 route_dist=route_dist)
602 if not for_withdrawal:
603 pathattrs = self.pathattr_map
605 vpnv_path = self.VPN_PATH_CLASS(
608 src_ver_num=self.source_version_num,
610 nexthop=self.nexthop,
611 is_withdraw=for_withdrawal)
615 def __eq__(self, b_path):
616 if not isinstance(b_path, self.__class__):
618 if not self.route_family == b_path.route_family:
620 if not self.puid == b_path.puid:
622 if not self.label_list == b_path.label_list:
624 if not self.nexthop == b_path.nexthop:
626 if not self.pathattr_map == b_path.pathattr_map:
632 class ImportMap(object):
633 def match(self, vrf_path):
634 raise NotImplementedError()
637 class VrfNlriImportMap(ImportMap):
638 VRF_PATH_CLASS = None
641 def __init__(self, prefix):
642 assert self.VRF_PATH_CLASS is not None
643 assert self.NLRI_CLASS is not None
644 self._nlri = self.NLRI_CLASS(prefix)
646 def match(self, vrf_path):
647 if vrf_path.route_family != self.VRF_PATH_CLASS.ROUTE_FAMILY:
649 "vrf_paths route_family does not match importmaps"
650 "route_family. Applied to wrong table?")
653 return vrf_path.nlri == self._nlri
656 class VrfRtImportMap(ImportMap):
657 def __init__(self, rt):
660 def match(self, vrf_path):
661 extcomm = vrf_path.pathattr_map.get(BGP_ATTR_TYPE_EXTENDED_COMMUNITIES)
662 return extcomm is not None and self._rt in extcomm.rt_list