1# Copyright (C) 2014 Nippon Telegraph and Telephone Corporation.
2#
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
6#
7#    http://www.apache.org/licenses/LICENSE-2.0
8#
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
12# implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""
17 Running or runtime configuration related to Virtual Routing and Forwarding
18 tables (VRFs).
19"""
20import abc
21import logging
22
23from ryu.lib.packet.bgp import RF_IPv4_UC
24from ryu.lib.packet.bgp import RF_IPv6_UC
25from ryu.lib.packet.bgp import RF_L2_EVPN
26from ryu.lib.packet.bgp import RF_IPv4_FLOWSPEC
27from ryu.lib.packet.bgp import RF_IPv6_FLOWSPEC
28from ryu.lib.packet.bgp import RF_L2VPN_FLOWSPEC
29
30from ryu.services.protocols.bgp.utils import validation
31from ryu.services.protocols.bgp.base import get_validator
32from ryu.services.protocols.bgp.rtconf.base import BaseConf
33from ryu.services.protocols.bgp.rtconf.base import BaseConfListener
34from ryu.services.protocols.bgp.rtconf.base import ConfigTypeError
35from ryu.services.protocols.bgp.rtconf.base import ConfigValueError
36from ryu.services.protocols.bgp.rtconf.base import ConfWithId
37from ryu.services.protocols.bgp.rtconf.base import ConfWithIdListener
38from ryu.services.protocols.bgp.rtconf.base import ConfWithStats
39from ryu.services.protocols.bgp.rtconf.base import ConfWithStatsListener
40from ryu.services.protocols.bgp.rtconf.base import MAX_NUM_EXPORT_RT
41from ryu.services.protocols.bgp.rtconf.base import MAX_NUM_IMPORT_RT
42from ryu.services.protocols.bgp.rtconf.base import MULTI_EXIT_DISC
43from ryu.services.protocols.bgp.rtconf.base import RuntimeConfigError
44from ryu.services.protocols.bgp.rtconf.base import SITE_OF_ORIGINS
45from ryu.services.protocols.bgp.rtconf.base import validate
46from ryu.services.protocols.bgp.rtconf.base import validate_med
47from ryu.services.protocols.bgp.rtconf.base import validate_soo_list
48
49
50LOG = logging.getLogger('bgpspeaker.rtconf.vrfs')
51
52# Configuration setting names.
53ROUTE_DISTINGUISHER = 'route_dist'
54IMPORT_RTS = 'import_rts'
55EXPORT_RTS = 'export_rts'
56VRF_NAME = 'vrf_name'
57VRF_DESC = 'vrf_desc'
58VRF_RF = 'route_family'
59IMPORT_MAPS = 'import_maps'
60
61# Supported VRF route-families
62VRF_RF_IPV4 = 'ipv4'
63VRF_RF_IPV6 = 'ipv6'
64VRF_RF_L2_EVPN = 'evpn'
65VRF_RF_IPV4_FLOWSPEC = 'ipv4fs'
66VRF_RF_IPV6_FLOWSPEC = 'ipv6fs'
67VRF_RF_L2VPN_FLOWSPEC = 'l2vpnfs'
68SUPPORTED_VRF_RF = (
69    VRF_RF_IPV4,
70    VRF_RF_IPV6,
71    VRF_RF_L2_EVPN,
72    VRF_RF_IPV4_FLOWSPEC,
73    VRF_RF_IPV6_FLOWSPEC,
74    VRF_RF_L2VPN_FLOWSPEC,
75)
76
77
78# Default configuration values.
79DEFAULT_VRF_NAME = 'no-vrf-name'
80DEFAULT_VRF_DESC = 'no-vrf-desc'
81
82
83@validate(name=IMPORT_RTS)
84def validate_import_rts(import_rts):
85    if not isinstance(import_rts, list):
86        raise ConfigTypeError(conf_name=IMPORT_RTS, conf_value=import_rts)
87    if not (len(import_rts) <= MAX_NUM_IMPORT_RT):
88        raise ConfigValueError(desc='Max. import RT is limited to %s' %
89                               MAX_NUM_IMPORT_RT)
90    if not all(validation.is_valid_ext_comm_attr(rt) for rt in import_rts):
91        raise ConfigValueError(conf_name=IMPORT_RTS, conf_value=import_rts)
92    # Check if we have duplicates
93    unique_rts = set(import_rts)
94    if len(unique_rts) != len(import_rts):
95        raise ConfigValueError(desc='Duplicate value provided %s' % import_rts)
96
97    return import_rts
98
99
100@validate(name=EXPORT_RTS)
101def validate_export_rts(export_rts):
102    if not isinstance(export_rts, list):
103        raise ConfigTypeError(conf_name=EXPORT_RTS, conf_value=export_rts)
104    if not (len(export_rts) <= MAX_NUM_EXPORT_RT):
105        raise ConfigValueError(desc='Max. import RT is limited to %s' %
106                               MAX_NUM_EXPORT_RT)
107
108    if not all(validation.is_valid_ext_comm_attr(rt) for rt in export_rts):
109        raise ConfigValueError(conf_name=EXPORT_RTS, conf_value=export_rts)
110    # Check if we have duplicates
111    unique_rts = set(export_rts)
112    if len(unique_rts) != len(export_rts):
113        raise ConfigValueError(desc='Duplicate value provided in %s' %
114                               export_rts)
115    return export_rts
116
117
118@validate(name=ROUTE_DISTINGUISHER)
119def validate_rd(route_dist):
120    if not validation.is_valid_route_dist(route_dist):
121        raise ConfigValueError(conf_name=ROUTE_DISTINGUISHER,
122                               conf_value=route_dist)
123    return route_dist
124
125
126@validate(name=VRF_RF)
127def validate_vrf_rf(vrf_rf):
128    if vrf_rf not in SUPPORTED_VRF_RF:
129        raise ConfigValueError(desc='Give VRF route family %s is not '
130                               'supported.' % vrf_rf)
131    return vrf_rf
132
133
134class VrfConf(ConfWithId, ConfWithStats):
135    """Class that encapsulates configurations for one VRF."""
136
137    VRF_CHG_EVT = 'vrf_chg_evt'
138
139    VALID_EVT = frozenset([VRF_CHG_EVT])
140
141    REQUIRED_SETTINGS = frozenset([ROUTE_DISTINGUISHER,
142                                   IMPORT_RTS,
143                                   EXPORT_RTS])
144
145    OPTIONAL_SETTINGS = frozenset(
146        [VRF_NAME, MULTI_EXIT_DISC, SITE_OF_ORIGINS, VRF_RF, IMPORT_MAPS]
147    )
148
149    def __init__(self, **kwargs):
150        """Create an instance of VRF runtime configuration."""
151        super(VrfConf, self).__init__(**kwargs)
152
153    def _init_opt_settings(self, **kwargs):
154        super(VrfConf, self)._init_opt_settings(**kwargs)
155        # We do not have valid default MED value.
156        # If no MED attribute is provided then we do not have to use MED.
157        # If MED attribute is provided we have to validate it and use it.
158        med = kwargs.pop(MULTI_EXIT_DISC, None)
159        if med and validate_med(med):
160            self._settings[MULTI_EXIT_DISC] = med
161
162        # We do not have valid default SOO value.
163        # If no SOO attribute is provided then we do not have to use SOO.
164        # If SOO attribute is provided we have to validate it and use it.
165        soos = kwargs.pop(SITE_OF_ORIGINS, None)
166        if soos and validate_soo_list(soos):
167            self._settings[SITE_OF_ORIGINS] = soos
168
169        # Current we we only support VRF for IPv4 and IPv6 with default IPv4
170        vrf_rf = kwargs.pop(VRF_RF, VRF_RF_IPV4)
171        if vrf_rf and validate_vrf_rf(vrf_rf):
172            self._settings[VRF_RF] = vrf_rf
173
174        import_maps = kwargs.pop(IMPORT_MAPS, [])
175        self._settings[IMPORT_MAPS] = import_maps
176
177    # =========================================================================
178    # Required attributes
179    # =========================================================================
180
181    @property
182    def route_dist(self):
183        return self._settings[ROUTE_DISTINGUISHER]
184
185    # =========================================================================
186    # Optional attributes with valid defaults.
187    # =========================================================================
188
189    @property
190    def import_rts(self):
191        return list(self._settings[IMPORT_RTS])
192
193    @property
194    def export_rts(self):
195        return list(self._settings[EXPORT_RTS])
196
197    @property
198    def soo_list(self):
199        soos = self._settings.get(SITE_OF_ORIGINS)
200        if soos:
201            soos = list(soos)
202        else:
203            soos = []
204        return soos
205
206    @property
207    def multi_exit_disc(self):
208        """Returns configured value of MED, else None.
209
210        This configuration does not have default value.
211        """
212        return self._settings.get(MULTI_EXIT_DISC)
213
214    @property
215    def route_family(self):
216        """Returns configured route family for this VRF
217
218        This configuration does not change.
219        """
220        return self._settings.get(VRF_RF)
221
222    @property
223    def rd_rf_id(self):
224        return VrfConf.create_rd_rf_id(self.route_dist, self.route_family)
225
226    @property
227    def import_maps(self):
228        return self._settings.get(IMPORT_MAPS)
229
230    @staticmethod
231    def create_rd_rf_id(route_dist, route_family):
232        return route_dist, route_family
233
234    @staticmethod
235    def vrf_rf_2_rf(vrf_rf):
236        if vrf_rf == VRF_RF_IPV4:
237            return RF_IPv4_UC
238        elif vrf_rf == VRF_RF_IPV6:
239            return RF_IPv6_UC
240        elif vrf_rf == VRF_RF_L2_EVPN:
241            return RF_L2_EVPN
242        elif vrf_rf == VRF_RF_IPV4_FLOWSPEC:
243            return RF_IPv4_FLOWSPEC
244        elif vrf_rf == VRF_RF_IPV6_FLOWSPEC:
245            return RF_IPv6_FLOWSPEC
246        elif vrf_rf == VRF_RF_L2VPN_FLOWSPEC:
247            return RF_L2VPN_FLOWSPEC
248        else:
249            raise ValueError('Unsupported VRF route family given %s' % vrf_rf)
250
251    @staticmethod
252    def rf_2_vrf_rf(route_family):
253        if route_family == RF_IPv4_UC:
254            return VRF_RF_IPV4
255        elif route_family == RF_IPv6_UC:
256            return VRF_RF_IPV6
257        elif route_family == RF_L2_EVPN:
258            return VRF_RF_L2_EVPN
259        elif route_family == RF_IPv4_FLOWSPEC:
260            return VRF_RF_IPV4_FLOWSPEC
261        elif route_family == RF_IPv6_FLOWSPEC:
262            return VRF_RF_IPV6_FLOWSPEC
263        elif route_family == RF_L2VPN_FLOWSPEC:
264            return VRF_RF_L2VPN_FLOWSPEC
265        else:
266            raise ValueError('No supported mapping for route family '
267                             'to vrf_route_family exists for %s' %
268                             route_family)
269
270    @property
271    def settings(self):
272        """Returns a copy of current settings.
273
274        As some of the attributes are themselves containers, we clone the
275        settings to provide clones for those containers as well.
276        """
277        # Shallow copy first
278        cloned_setting = self._settings.copy()
279        # Don't want clone to link to same RT containers
280        cloned_setting[IMPORT_RTS] = self.import_rts
281        cloned_setting[EXPORT_RTS] = self.export_rts
282        cloned_setting[SITE_OF_ORIGINS] = self.soo_list
283        return cloned_setting
284
285    @classmethod
286    def get_opt_settings(cls):
287        self_confs = super(VrfConf, cls).get_opt_settings()
288        self_confs.update(VrfConf.OPTIONAL_SETTINGS)
289        return self_confs
290
291    @classmethod
292    def get_req_settings(cls):
293        self_confs = super(VrfConf, cls).get_req_settings()
294        self_confs.update(VrfConf.REQUIRED_SETTINGS)
295        return self_confs
296
297    @classmethod
298    def get_valid_evts(cls):
299        self_valid_evts = super(VrfConf, cls).get_valid_evts()
300        self_valid_evts.update(VrfConf.VALID_EVT)
301        return self_valid_evts
302
303    def update(self, **kwargs):
304        """Updates this `VrfConf` settings.
305
306        Notifies listeners if any settings changed. Returns `True` if update
307        was successful. This vrfs' route family, id and route dist settings
308        cannot be updated/changed.
309        """
310        # Update inherited configurations
311        super(VrfConf, self).update(**kwargs)
312        vrf_id = kwargs.get(ConfWithId.ID)
313        vrf_rd = kwargs.get(ROUTE_DISTINGUISHER)
314        vrf_rf = kwargs.get(VRF_RF)
315        if (vrf_id != self.id or
316                vrf_rd != self.route_dist or
317                vrf_rf != self.route_family):
318            raise ConfigValueError(desc='id/route-distinguisher/route-family'
319                                   ' do not match configured value.')
320
321        # Validate and update individual settings
322        new_imp_rts, old_imp_rts = \
323            self._update_import_rts(**kwargs)
324        export_rts_changed = self._update_export_rts(**kwargs)
325        soos_list_changed = self._update_soo_list(**kwargs)
326        med_changed = self._update_med(**kwargs)
327        re_export_needed = (export_rts_changed or
328                            soos_list_changed or
329                            med_changed)
330        import_maps = kwargs.get(IMPORT_MAPS, [])
331        re_import_needed = self._update_importmaps(import_maps)
332
333        # If we did have any change in value of any settings, we notify
334        # listeners
335        if (new_imp_rts is not None or
336                old_imp_rts is not None or
337                re_export_needed or re_import_needed):
338            evt_value = (
339                new_imp_rts,
340                old_imp_rts,
341                import_maps,
342                re_export_needed,
343                re_import_needed
344            )
345            self._notify_listeners(VrfConf.VRF_CHG_EVT, evt_value)
346        return True
347
348    def _update_import_rts(self, **kwargs):
349        import_rts = kwargs.get(IMPORT_RTS)
350        get_validator(IMPORT_RTS)(import_rts)
351        curr_import_rts = set(self._settings[IMPORT_RTS])
352
353        import_rts = set(import_rts)
354        if not import_rts.symmetric_difference(curr_import_rts):
355            return None, None
356
357        # Get the difference between current and new RTs
358        new_import_rts = import_rts - curr_import_rts
359        old_import_rts = curr_import_rts - import_rts
360
361        # Update current RTs and notify listeners.
362        self._settings[IMPORT_RTS] = import_rts
363        return new_import_rts, old_import_rts
364
365    def _update_export_rts(self, **kwargs):
366        export_rts = kwargs.get(EXPORT_RTS)
367        get_validator(EXPORT_RTS)(export_rts)
368        curr_export_rts = set(self._settings[EXPORT_RTS])
369
370        if curr_export_rts.symmetric_difference(export_rts):
371            # Update current RTs and notify listeners.
372            self._settings[EXPORT_RTS] = list(export_rts)
373            return True
374
375        return False
376
377    def _update_soo_list(self, **kwargs):
378        soo_list = kwargs.get(SITE_OF_ORIGINS, [])
379        get_validator(SITE_OF_ORIGINS)(soo_list)
380        curr_soos = set(self.soo_list)
381
382        # If given list is different from existing settings, we update it
383        if curr_soos.symmetric_difference(soo_list):
384            self._settings[SITE_OF_ORIGINS] = soo_list[:]
385            return True
386
387        return False
388
389    def _update_med(self, **kwargs):
390        multi_exit_disc = kwargs.get(MULTI_EXIT_DISC, None)
391        if multi_exit_disc:
392            get_validator(MULTI_EXIT_DISC)(multi_exit_disc)
393
394        if multi_exit_disc != self.multi_exit_disc:
395            self._settings[MULTI_EXIT_DISC] = multi_exit_disc
396            return True
397
398        return False
399
400    def _update_importmaps(self, import_maps):
401        if set(self._settings[IMPORT_MAPS]).symmetric_difference(import_maps):
402            self._settings[IMPORT_MAPS] = import_maps
403            return True
404
405        return False
406
407    def __repr__(self):
408        return ('<%s(route_dist: %r, import_rts: %r, export_rts: %r, '
409                'soo_list: %r)>' % (self.__class__.__name__,
410                                    self.route_dist, self.import_rts,
411                                    self.export_rts, self.soo_list))
412
413    def __str__(self):
414        return 'VrfConf-%s' % self.route_dist
415
416
417class VrfsConf(BaseConf):
418    """Container for all VRF configurations."""
419
420    ADD_VRF_CONF_EVT, REMOVE_VRF_CONF_EVT = range(2)
421
422    VALID_EVT = frozenset([ADD_VRF_CONF_EVT, REMOVE_VRF_CONF_EVT])
423
424    def __init__(self):
425        super(VrfsConf, self).__init__()
426        self._vrfs_by_rd_rf = {}
427        self._vrfs_by_id = {}
428
429    def _init_opt_settings(self, **kwargs):
430        pass
431
432    @property
433    def vrf_confs(self):
434        """Returns a list of configured `VrfConf`s
435        """
436        return list(self._vrfs_by_rd_rf.values())
437
438    @property
439    def vrf_interested_rts(self):
440        interested_rts = set()
441        for vrf_conf in self._vrfs_by_id.values():
442            interested_rts.update(vrf_conf.import_rts)
443        return interested_rts
444
445    def update(self, **kwargs):
446        raise NotImplementedError('Use either add/remove_vrf_conf'
447                                  ' methods instead.')
448
449    def add_vrf_conf(self, vrf_conf):
450        if vrf_conf.rd_rf_id in self._vrfs_by_rd_rf.keys():
451            raise RuntimeConfigError(
452                desc='VrfConf with rd_rf %s already exists'
453                     % str(vrf_conf.rd_rf_id)
454            )
455        if vrf_conf.id in self._vrfs_by_id:
456            raise RuntimeConfigError(
457                desc='VrfConf with id %s already exists' % str(vrf_conf.id)
458            )
459
460        self._vrfs_by_rd_rf[vrf_conf.rd_rf_id] = vrf_conf
461        self._vrfs_by_id[vrf_conf.id] = vrf_conf
462        self._notify_listeners(VrfsConf.ADD_VRF_CONF_EVT, vrf_conf)
463
464    def remove_vrf_conf(self, route_dist=None, vrf_id=None,
465                        vrf_rf=None):
466        """Removes any matching `VrfConf` for given `route_dist` or `vrf_id`
467
468        Parameters:
469            - `route_dist`: (str) route distinguisher of a configured VRF
470            - `vrf_id`: (str) vrf ID
471            - `vrf_rf`: (str) route family of the VRF configuration
472        If only `route_dist` is given, removes `VrfConf`s for all supported
473        address families for this `route_dist`. If `vrf_rf` is given, than only
474        removes `VrfConf` for that specific route family. If only `vrf_id` is
475        given, matching `VrfConf` will be removed.
476        """
477        if route_dist is None and vrf_id is None:
478            raise RuntimeConfigError(desc='To delete supply route_dist or id.')
479
480        # By default we remove all VRFs for given Id or RD
481        vrf_rfs = SUPPORTED_VRF_RF
482        # If asked to delete specific route family vrf conf.
483        if vrf_rf:
484            vrf_rfs = vrf_rf
485
486        # For all vrf route family asked to be deleted, we collect all deleted
487        # VrfConfs
488        removed_vrf_confs = []
489        for route_family in vrf_rfs:
490            if route_dist is not None:
491                rd_rf_id = VrfConf.create_rd_rf_id(route_dist, route_family)
492                vrf_conf = self._vrfs_by_rd_rf.pop(rd_rf_id, None)
493                if vrf_conf:
494                    self._vrfs_by_id.pop(vrf_conf.id, None)
495                    removed_vrf_confs.append(vrf_conf)
496            else:
497                vrf_conf = self._vrfs_by_id.pop(vrf_id, None)
498                if vrf_conf:
499                    self._vrfs_by_rd_rf.pop(vrf_conf.rd_rd_id, None)
500                    removed_vrf_confs.append(vrf_conf)
501
502        # We do not raise any exception if we cannot find asked VRF.
503        for vrf_conf in removed_vrf_confs:
504            self._notify_listeners(VrfsConf.REMOVE_VRF_CONF_EVT, vrf_conf)
505        return removed_vrf_confs
506
507    def get_vrf_conf(self, route_dist, vrf_rf, vrf_id=None):
508        if route_dist is None and vrf_id is None:
509            raise RuntimeConfigError(desc='To get VRF supply route_dist '
510                                     'or vrf_id.')
511        if route_dist is not None and vrf_id is not None:
512            vrf1 = self._vrfs_by_id.get(vrf_id)
513            rd_rf_id = VrfConf.create_rd_rf_id(route_dist, vrf_rf)
514            vrf2 = self._vrfs_by_rd_rf.get(rd_rf_id)
515            if vrf1 is not vrf2:
516                raise RuntimeConfigError(desc='Given VRF ID (%s) and RD (%s)'
517                                         ' are not of same VRF.' %
518                                         (vrf_id, route_dist))
519            vrf = vrf1
520        elif route_dist is not None:
521            rd_rf_id = VrfConf.create_rd_rf_id(route_dist, vrf_rf)
522            vrf = self._vrfs_by_rd_rf.get(rd_rf_id)
523        else:
524            vrf = self._vrfs_by_id.get(vrf_id)
525        return vrf
526
527    @property
528    def vrfs_by_rd_rf_id(self):
529        return dict(self._vrfs_by_rd_rf)
530
531    @classmethod
532    def get_valid_evts(cls):
533        self_valid_evts = super(VrfsConf, cls).get_valid_evts()
534        self_valid_evts.update(VrfsConf.VALID_EVT)
535        return self_valid_evts
536
537    def __repr__(self):
538        return '<%s(%r)>' % (self.__class__.__name__, self._vrfs_by_id)
539
540    @property
541    def settings(self):
542        return [vrf.settings for vrf in self._vrfs_by_id.values()]
543
544
545class VrfConfListener(ConfWithIdListener, ConfWithStatsListener):
546    """Base listener for various VRF configuration change event."""
547
548    def __init__(self, vrf_conf):
549        super(VrfConfListener, self).__init__(vrf_conf)
550        vrf_conf.add_listener(VrfConf.VRF_CHG_EVT, self.on_chg_vrf_conf)
551
552    def on_chg_vrf_conf(self, evt):
553        raise NotImplementedError('This method should be overridden')
554
555
556class VrfsConfListener(BaseConfListener):
557    """Base listener for VRF container change events."""
558
559    def __init__(self, vrfs_conf):
560        super(VrfsConfListener, self).__init__(vrfs_conf)
561        vrfs_conf.add_listener(VrfsConf.ADD_VRF_CONF_EVT, self.on_add_vrf_conf)
562        vrfs_conf.add_listener(VrfsConf.REMOVE_VRF_CONF_EVT,
563                               self.on_remove_vrf_conf)
564
565    @abc.abstractmethod
566    def on_add_vrf_conf(self, evt):
567        raise NotImplementedError('This method should be overridden')
568
569    @abc.abstractmethod
570    def on_remove_vrf_conf(self, evt):
571        raise NotImplementedError('This method should be overridden')
572