mirror of
https://github.com/faucetsdn/ryu.git
synced 2026-01-23 01:21:31 +01:00
BGPSpeaker: Support VRF Table for Ethernet VPN
This patch enables BGPSpeaker to store EVPN routes into the VRF
tables and to provide the API for advertising routes.
Usage example:
speaker = BGPSpeaker(as_number=65001,
router_id='172.17.0.1')
speaker.neighbor_add(address='172.17.0.2', remote_as=65002,
enable_evpn=True)
speaker.vrf_add(route_dist='65001:100',
import_rts=['65001:100'],
export_rts=['65001:100'],
route_family=RF_L2_EVPN)
speaker.evpn_prefix_add(route_type=EVPN_MAC_IP_ADV_ROUTE,
route_dist='65001:100',
esi=0,
ethernet_tag_id=200,
mac_addr='aa:bb:cc:dd:ee:ff',
ip_addr='10.0.0.1',
next_hop='172.19.0.1')
Signed-off-by: IWASE Yusuke <iwase.yusuke0@gmail.com>
Signed-off-by: FUJITA Tomonori <fujita.tomonori@lab.ntt.co.jp>
This commit is contained in:
parent
59a3049e13
commit
b8e75e7e7b
@ -40,6 +40,7 @@ from ryu.services.protocols.bgp.rtconf.base import ConfigValueError
|
||||
from ryu.services.protocols.bgp.rtconf.base import RuntimeConfigError
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF_IPV4
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF_L2_EVPN
|
||||
from ryu.services.protocols.bgp.utils import validation
|
||||
|
||||
|
||||
@ -125,7 +126,7 @@ def add_local(route_dist, prefix, next_hop, route_family=VRF_RF_IPV4):
|
||||
try:
|
||||
# Create new path and insert into appropriate VRF table.
|
||||
tm = CORE_MANAGER.get_core_service().table_manager
|
||||
label = tm.add_to_vrf(route_dist, prefix, next_hop, route_family)
|
||||
label = tm.update_vrf_table(route_dist, prefix, next_hop, route_family)
|
||||
# Currently we only allocate one label per local_prefix,
|
||||
# so we share first label from the list.
|
||||
if label:
|
||||
@ -147,8 +148,9 @@ def delete_local(route_dist, prefix, route_family=VRF_RF_IPV4):
|
||||
"""
|
||||
try:
|
||||
tm = CORE_MANAGER.get_core_service().table_manager
|
||||
tm.remove_from_vrf(route_dist, prefix, route_family)
|
||||
# Send success response to ApgwAgent.
|
||||
tm.update_vrf_table(route_dist, prefix,
|
||||
route_family=route_family, is_withdraw=True)
|
||||
# Send success response.
|
||||
return [{ROUTE_DISTINGUISHER: route_dist, PREFIX: prefix,
|
||||
VRF_RF: route_family}]
|
||||
except BgpCoreError as e:
|
||||
@ -165,9 +167,26 @@ def delete_local(route_dist, prefix, route_family=VRF_RF_IPV4):
|
||||
opt_args=[EVPN_ESI, EVPN_ETHERNET_TAG_ID, MAC_ADDR,
|
||||
IP_ADDR])
|
||||
def add_evpn_local(route_type, route_dist, next_hop, **kwargs):
|
||||
tm = CORE_MANAGER.get_core_service().table_manager
|
||||
tm.add_to_global_evpn_table(route_type, route_dist, next_hop, **kwargs)
|
||||
return True
|
||||
"""Adds EVPN route from VRF identified by *route_dist*.
|
||||
"""
|
||||
try:
|
||||
# Create new path and insert into appropriate VRF table.
|
||||
tm = CORE_MANAGER.get_core_service().table_manager
|
||||
label = tm.update_vrf_table(route_dist, next_hop=next_hop,
|
||||
route_family=VRF_RF_L2_EVPN,
|
||||
route_type=route_type, **kwargs)
|
||||
# Currently we only allocate one label per local route,
|
||||
# so we share first label from the list.
|
||||
if label:
|
||||
label = label[0]
|
||||
|
||||
# Send success response with new label.
|
||||
return [{EVPN_ROUTE_TYPE: route_type,
|
||||
ROUTE_DISTINGUISHER: route_dist,
|
||||
VRF_RF: VRF_RF_L2_EVPN,
|
||||
VPN_LABEL: label}.update(kwargs)]
|
||||
except BgpCoreError as e:
|
||||
raise PrefixError(desc=e)
|
||||
|
||||
|
||||
@RegisterWithArgChecks(name='evpn_prefix.delete_local',
|
||||
@ -175,7 +194,16 @@ def add_evpn_local(route_type, route_dist, next_hop, **kwargs):
|
||||
opt_args=[EVPN_ESI, EVPN_ETHERNET_TAG_ID, MAC_ADDR,
|
||||
IP_ADDR])
|
||||
def delete_evpn_local(route_type, route_dist, **kwargs):
|
||||
tm = CORE_MANAGER.get_core_service().table_manager
|
||||
tm.add_to_global_evpn_table(route_type, route_dist, is_withdraw=True,
|
||||
**kwargs)
|
||||
return True
|
||||
"""Deletes/withdraws EVPN route from VRF identified by *route_dist*.
|
||||
"""
|
||||
try:
|
||||
tm = CORE_MANAGER.get_core_service().table_manager
|
||||
tm.update_vrf_table(route_dist,
|
||||
route_family=VRF_RF_L2_EVPN,
|
||||
route_type=route_type, is_withdraw=True, **kwargs)
|
||||
# Send success response.
|
||||
return [{EVPN_ROUTE_TYPE: route_type,
|
||||
ROUTE_DISTINGUISHER: route_dist,
|
||||
VRF_RF: VRF_RF_L2_EVPN}.update(kwargs)]
|
||||
except BgpCoreError as e:
|
||||
raise PrefixError(desc=e)
|
||||
|
||||
@ -70,6 +70,7 @@ from ryu.services.protocols.bgp.rtconf.neighbors import IS_NEXT_HOP_SELF
|
||||
from ryu.services.protocols.bgp.rtconf.neighbors import CONNECT_MODE
|
||||
from ryu.services.protocols.bgp.rtconf.neighbors import LOCAL_ADDRESS
|
||||
from ryu.services.protocols.bgp.rtconf.neighbors import LOCAL_PORT
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import SUPPORTED_VRF_RF
|
||||
from ryu.services.protocols.bgp.info_base.base import Filter
|
||||
from ryu.services.protocols.bgp.info_base.ipv4 import Ipv4Path
|
||||
from ryu.services.protocols.bgp.info_base.ipv6 import Ipv6Path
|
||||
@ -80,6 +81,7 @@ from ryu.services.protocols.bgp.info_base.vpnv6 import Vpnv6Path
|
||||
NEIGHBOR_CONF_MED = 'multi_exit_disc'
|
||||
RF_VPN_V4 = vrfs.VRF_RF_IPV4
|
||||
RF_VPN_V6 = vrfs.VRF_RF_IPV6
|
||||
RF_L2_EVPN = vrfs.VRF_RF_L2_EVPN
|
||||
|
||||
|
||||
class EventPrefix(object):
|
||||
@ -588,29 +590,30 @@ class BGPSpeaker(object):
|
||||
This parameter must be a list of string.
|
||||
|
||||
``route_family`` specifies route family of the VRF.
|
||||
This parameter must be RF_VPN_V4 or RF_VPN_V6.
|
||||
This parameter must be RF_VPN_V4, RF_VPN_V6 or RF_L2_EVPN.
|
||||
"""
|
||||
|
||||
assert route_family in (RF_VPN_V4, RF_VPN_V6),\
|
||||
'route_family must be RF_VPN_V4 or RF_VPN_V6'
|
||||
assert route_family in SUPPORTED_VRF_RF,\
|
||||
'route_family must be RF_VPN_V4, RF_VPN_V6 or RF_L2_EVPN'
|
||||
|
||||
vrf = {
|
||||
vrfs.ROUTE_DISTINGUISHER: route_dist,
|
||||
vrfs.IMPORT_RTS: import_rts,
|
||||
vrfs.EXPORT_RTS: export_rts,
|
||||
vrfs.SITE_OF_ORIGINS: site_of_origins,
|
||||
vrfs.VRF_RF: route_family,
|
||||
}
|
||||
|
||||
vrf = {}
|
||||
vrf[vrfs.ROUTE_DISTINGUISHER] = route_dist
|
||||
vrf[vrfs.IMPORT_RTS] = import_rts
|
||||
vrf[vrfs.EXPORT_RTS] = export_rts
|
||||
vrf[vrfs.SITE_OF_ORIGINS] = site_of_origins
|
||||
vrf[vrfs.VRF_RF] = route_family
|
||||
call('vrf.create', **vrf)
|
||||
|
||||
def vrf_del(self, route_dist):
|
||||
""" This method deletes the existing vrf.
|
||||
|
||||
``route_dist`` specifies a route distinguisher value.
|
||||
|
||||
"""
|
||||
|
||||
vrf = {}
|
||||
vrf[vrfs.ROUTE_DISTINGUISHER] = route_dist
|
||||
vrf = {vrfs.ROUTE_DISTINGUISHER: route_dist}
|
||||
|
||||
call('vrf.delete', **vrf)
|
||||
|
||||
def vrfs_get(self, format='json'):
|
||||
|
||||
@ -15,11 +15,12 @@ from ryu.services.protocols.bgp.info_base.vpnv6 import Vpnv6Path
|
||||
from ryu.services.protocols.bgp.info_base.vpnv6 import Vpnv6Table
|
||||
from ryu.services.protocols.bgp.info_base.vrf4 import Vrf4Table
|
||||
from ryu.services.protocols.bgp.info_base.vrf6 import Vrf6Table
|
||||
from ryu.services.protocols.bgp.info_base.evpn import EvpnPath
|
||||
from ryu.services.protocols.bgp.info_base.vrfevpn import VrfEvpnTable
|
||||
from ryu.services.protocols.bgp.info_base.evpn import EvpnTable
|
||||
from ryu.services.protocols.bgp.rtconf import vrfs
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF_IPV4
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF_IPV6
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import VRF_RF_L2_EVPN
|
||||
from ryu.services.protocols.bgp.rtconf.vrfs import SUPPORTED_VRF_RF
|
||||
|
||||
from ryu.lib import type_desc
|
||||
@ -102,24 +103,25 @@ class TableCoreManager(object):
|
||||
LOG.debug('VRF with RD %s marked for removal', vrf_conf.route_dist)
|
||||
|
||||
def import_all_vpn_paths_to_vrf(self, vrf_table, import_rts=None):
|
||||
"""Imports Vpnv4/6 paths from Global/VPN table into given Vrfv4/6
|
||||
table.
|
||||
"""Imports VPNv4/6 or EVPN paths from Global/VPN table into given
|
||||
VRFv4/6 or VRFEVPN table.
|
||||
:param vrf_table: Vrf table to which we import
|
||||
:type vrf_table: VrfTable
|
||||
:param import_rts: import RTs to override default import_rts of
|
||||
vrf table for this import
|
||||
:type import_rts: set of strings
|
||||
|
||||
|
||||
Checks if we have any path RT common with VRF table's import RT.
|
||||
"""
|
||||
rfs = (Vrf4Table.ROUTE_FAMILY, Vrf6Table.ROUTE_FAMILY)
|
||||
assert vrf_table.route_family in rfs, 'Invalid VRF table.'
|
||||
|
||||
if vrf_table.route_family == Vrf4Table.ROUTE_FAMILY:
|
||||
vpn_table = self.get_vpn4_table()
|
||||
else:
|
||||
elif vrf_table.route_family == Vrf6Table.ROUTE_FAMILY:
|
||||
vpn_table = self.get_vpn6_table()
|
||||
elif vrf_table.route_family == VrfEvpnTable.ROUTE_FAMILY:
|
||||
vpn_table = self.get_evpn_table()
|
||||
else:
|
||||
raise ValueError('Invalid VRF table route family: %s' %
|
||||
vrf_table.route_family)
|
||||
|
||||
vrf_table.import_vpn_paths_from_table(vpn_table, import_rts)
|
||||
|
||||
@ -320,8 +322,8 @@ class TableCoreManager(object):
|
||||
for path in dest.known_path_list:
|
||||
if path.source is None:
|
||||
vrf_table.insert_vrf_path(
|
||||
path.nlri,
|
||||
path.nexthop,
|
||||
nlri=path.nlri,
|
||||
next_hop=path.nexthop,
|
||||
gen_lbl=True
|
||||
)
|
||||
LOG.debug('Re-installed NC paths with current policy for table %s.',
|
||||
@ -364,26 +366,24 @@ class TableCoreManager(object):
|
||||
importing/installing of paths from global tables.
|
||||
Returns created table.
|
||||
"""
|
||||
|
||||
route_family = vrf_conf.route_family
|
||||
assert route_family in (VRF_RF_IPV4, VRF_RF_IPV6)
|
||||
vrf_table = None
|
||||
if route_family == VRF_RF_IPV4:
|
||||
vrf_table = Vrf4Table(
|
||||
vrf_conf, self._core_service, self._signal_bus
|
||||
)
|
||||
table_id = (vrf_conf.route_dist, route_family)
|
||||
self._tables[table_id] = vrf_table
|
||||
|
||||
if route_family == VRF_RF_IPV4:
|
||||
vrf_table = Vrf4Table
|
||||
elif route_family == VRF_RF_IPV6:
|
||||
vrf_table = Vrf6Table(
|
||||
vrf_conf, self._core_service, self._signal_bus
|
||||
)
|
||||
table_id = (vrf_conf.route_dist, route_family)
|
||||
self._tables[table_id] = vrf_table
|
||||
vrf_table = Vrf6Table
|
||||
elif route_family == VRF_RF_L2_EVPN:
|
||||
vrf_table = VrfEvpnTable
|
||||
else:
|
||||
raise ValueError('Unsupported route family for VRF: %s' %
|
||||
route_family)
|
||||
|
||||
vrf_table = vrf_table(vrf_conf, self._core_service, self._signal_bus)
|
||||
table_id = (vrf_conf.route_dist, route_family)
|
||||
self._tables[table_id] = vrf_table
|
||||
|
||||
assert vrf_table is not None
|
||||
LOG.debug('Added new VrfTable with rd: %s and add_fmly: %s',
|
||||
LOG.debug('Added new VrfTable with route_dist:%s and route_family:%s',
|
||||
vrf_conf.route_dist, route_family)
|
||||
|
||||
import_rts = vrf_conf.import_rts
|
||||
@ -435,13 +435,11 @@ class TableCoreManager(object):
|
||||
uninteresting_dest_count)
|
||||
|
||||
def import_single_vpn_path_to_all_vrfs(self, vpn_path, path_rts=None):
|
||||
"""Imports *vpnv4_path* to qualifying VRF tables.
|
||||
"""Imports *vpn_path* to qualifying VRF tables.
|
||||
|
||||
Import RTs of VRF table is matched with RTs from *vpn4_path* and if we
|
||||
have any common RTs we import the path into VRF.
|
||||
"""
|
||||
assert (vpn_path.route_family in
|
||||
(Vpnv4Path.ROUTE_FAMILY, Vpnv6Path.ROUTE_FAMILY))
|
||||
LOG.debug('Importing path %s to qualifying VRFs', vpn_path)
|
||||
|
||||
# If this path has no RTs we are done.
|
||||
@ -453,9 +451,16 @@ class TableCoreManager(object):
|
||||
interested_tables = set()
|
||||
|
||||
# Get route family of VRF to when this VPN Path can be imported to
|
||||
route_family = RF_IPv4_UC
|
||||
if vpn_path.route_family != RF_IPv4_VPN:
|
||||
if vpn_path.route_family == RF_IPv4_VPN:
|
||||
route_family = RF_IPv4_UC
|
||||
elif vpn_path.route_family == RF_IPv6_VPN:
|
||||
route_family = RF_IPv6_UC
|
||||
elif vpn_path.route_family == RF_L2_EVPN:
|
||||
route_family = RF_L2_EVPN
|
||||
else:
|
||||
raise ValueError('Unsupported route family for VRF: %s' %
|
||||
vpn_path.route_family)
|
||||
|
||||
for rt in path_rts:
|
||||
rt_rf_id = rt + ':' + str(route_family)
|
||||
vrf_rt_tables = self._tables_for_rt.get(rt_rf_id)
|
||||
@ -478,44 +483,70 @@ class TableCoreManager(object):
|
||||
# If we do not have any VRF with import RT that match with path RT
|
||||
LOG.debug('No VRF table found that imports RTs: %s', path_rts)
|
||||
|
||||
def add_to_vrf(self, route_dist, prefix, next_hop, route_family):
|
||||
"""Adds `prefix` to VRF identified by `route_dist` with given
|
||||
`next_hop`.
|
||||
def update_vrf_table(self, route_dist, prefix=None, next_hop=None,
|
||||
route_family=None, route_type=None,
|
||||
is_withdraw=False, **kwargs):
|
||||
"""Update a BGP route in the VRF table identified by `route_dist`
|
||||
with the given `next_hop`.
|
||||
|
||||
If `is_withdraw` is False, which is the default, add a BGP route
|
||||
to the VRF table identified by `route_dist` with the given
|
||||
`next_hop`.
|
||||
If `is_withdraw` is True, remove a BGP route from the VRF table
|
||||
and the given `next_hop` is ignored.
|
||||
|
||||
If `route_family` is VRF_RF_L2_EVPN, `route_type` and `kwargs`
|
||||
are required to construct EVPN NLRI and `prefix` is ignored.
|
||||
|
||||
Returns assigned VPN label.
|
||||
"""
|
||||
from ryu.services.protocols.bgp.core import BgpCoreError
|
||||
|
||||
assert route_dist and prefix and next_hop
|
||||
if route_family not in (VRF_RF_IPV4, VRF_RF_IPV6):
|
||||
raise ValueError('Given route_family %s is not supported.' %
|
||||
route_family)
|
||||
assert route_dist
|
||||
|
||||
if is_withdraw:
|
||||
gen_lbl = False
|
||||
next_hop = None
|
||||
else:
|
||||
gen_lbl = True
|
||||
if not (is_valid_ipv4(next_hop) or is_valid_ipv6(next_hop)):
|
||||
raise BgpCoreError(
|
||||
desc='Invalid IPv4/IPv6 nexthop: %s' % next_hop)
|
||||
|
||||
vrf_table = self._tables.get((route_dist, route_family))
|
||||
if vrf_table is None:
|
||||
raise BgpCoreError(
|
||||
desc='VRF table does not exist: route_dist=%s, '
|
||||
'route_family=%s' % (route_dist, route_family))
|
||||
|
||||
vrf_table = None
|
||||
table_id = (route_dist, route_family)
|
||||
if route_family == VRF_RF_IPV4:
|
||||
vrf_table = self._tables.get(table_id)
|
||||
if vrf_table is None:
|
||||
raise BgpCoreError(desc='VRF table for RD: %s does not '
|
||||
'exist.' % route_dist)
|
||||
if not is_valid_ipv4_prefix(prefix) or not is_valid_ipv4(next_hop):
|
||||
raise BgpCoreError(desc='Invalid Ipv4 prefix or nexthop.')
|
||||
if not is_valid_ipv4_prefix(prefix):
|
||||
raise BgpCoreError(desc='Invalid IPv4 prefix: %s' % prefix)
|
||||
ip, masklen = prefix.split('/')
|
||||
prefix = IPAddrPrefix(int(masklen), ip)
|
||||
elif route_family == VRF_RF_IPV6:
|
||||
vrf_table = self._tables.get(table_id)
|
||||
if vrf_table is None:
|
||||
raise BgpCoreError(desc='VRF table for RD: %s does not '
|
||||
'exist.' % route_dist)
|
||||
if not is_valid_ipv6_prefix(prefix) or not is_valid_ipv6(next_hop):
|
||||
raise BgpCoreError(desc='Invalid Ipv6 prefix or nexthop.')
|
||||
if not is_valid_ipv6_prefix(prefix):
|
||||
raise BgpCoreError(desc='Invalid IPv6 prefix: %s' % prefix)
|
||||
ip6, masklen = prefix.split('/')
|
||||
prefix = IP6AddrPrefix(int(masklen), ip6)
|
||||
elif route_family == VRF_RF_L2_EVPN:
|
||||
assert route_type
|
||||
subclass = EvpnNLRI._lookup_type_name(route_type)
|
||||
kwargs['route_dist'] = route_dist
|
||||
esi = kwargs.get('esi', None)
|
||||
if esi is not None:
|
||||
# Note: Currently, we support arbitrary 9-octet ESI value only.
|
||||
kwargs['esi'] = EvpnArbitraryEsi(type_desc.Int9.from_user(esi))
|
||||
prefix = subclass(**kwargs)
|
||||
else:
|
||||
raise BgpCoreError(
|
||||
desc='Unsupported route family %s' % route_family)
|
||||
|
||||
# We do not check if we have a path to given prefix, we issue
|
||||
# withdrawal. Hence multiple withdrawals have not side effect.
|
||||
return vrf_table.insert_vrf_path(
|
||||
prefix, next_hop=next_hop,
|
||||
gen_lbl=True
|
||||
)
|
||||
nlri=prefix, next_hop=next_hop, gen_lbl=gen_lbl,
|
||||
is_withdraw=is_withdraw)
|
||||
|
||||
def add_to_global_table(self, prefix, nexthop=None,
|
||||
is_withdraw=False):
|
||||
@ -550,78 +581,6 @@ class TableCoreManager(object):
|
||||
# add to global ipv4 table and propagates to neighbors
|
||||
self.learn_path(new_path)
|
||||
|
||||
def add_to_global_evpn_table(self, route_type, route_dist, next_hop=None,
|
||||
is_withdraw=False, **kwargs):
|
||||
"""Adds BGP EVPN Route to global EVPN Table with given `next_hop`.
|
||||
|
||||
If `is_withdraw` is set to `True`, removes the given route from
|
||||
global EVPN Table.
|
||||
"""
|
||||
|
||||
# construct EVPN NLRI instance
|
||||
subclass = EvpnNLRI._lookup_type_name(route_type)
|
||||
kwargs['route_dist'] = route_dist
|
||||
esi = kwargs.get('esi', None)
|
||||
if esi is not None:
|
||||
# Note: Currently, we support arbitrary 9-octet ESI value only.
|
||||
kwargs['esi'] = EvpnArbitraryEsi(type_desc.Int9.from_user(esi))
|
||||
nlri = subclass(**kwargs)
|
||||
|
||||
# set mandatory path attributes
|
||||
origin = BGPPathAttributeOrigin(BGP_ATTR_ORIGIN_IGP)
|
||||
aspath = BGPPathAttributeAsPath([[]])
|
||||
pathattrs = OrderedDict()
|
||||
pathattrs[BGP_ATTR_TYPE_ORIGIN] = origin
|
||||
pathattrs[BGP_ATTR_TYPE_AS_PATH] = aspath
|
||||
|
||||
# set the default next_hop address
|
||||
if next_hop is None:
|
||||
next_hop = '0.0.0.0'
|
||||
|
||||
new_path = EvpnPath(source=None, nlri=nlri, src_ver_num=1,
|
||||
pattrs=pathattrs, nexthop=next_hop,
|
||||
is_withdraw=is_withdraw)
|
||||
|
||||
# add to global EVPN table and propagates to neighbors
|
||||
self.learn_path(new_path)
|
||||
|
||||
def remove_from_vrf(self, route_dist, prefix, route_family):
|
||||
"""Removes `prefix` from VRF identified by `route_dist`.
|
||||
|
||||
Returns assigned VPN label.
|
||||
"""
|
||||
from ryu.services.protocols.bgp.core import BgpCoreError
|
||||
# Validate given
|
||||
if route_family not in (VRF_RF_IPV4, VRF_RF_IPV6):
|
||||
raise BgpCoreError(desc='Unsupported route family %s' %
|
||||
route_family)
|
||||
val_ipv4 = route_family == VRF_RF_IPV4\
|
||||
and is_valid_ipv4_prefix(prefix)
|
||||
val_ipv6 = route_family == VRF_RF_IPV6\
|
||||
and is_valid_ipv6_prefix(prefix)
|
||||
|
||||
if not val_ipv4 and not val_ipv6:
|
||||
raise BgpCoreError(desc='Invalid prefix or nexthop.')
|
||||
|
||||
table_id = (route_dist, route_family)
|
||||
if route_family == VRF_RF_IPV4:
|
||||
vrf_table = self._tables.get(table_id)
|
||||
if not vrf_table:
|
||||
raise BgpCoreError(desc='Vrf for route distinguisher %s does '
|
||||
'not exist.' % route_dist)
|
||||
ip, masklen = prefix.split('/')
|
||||
prefix = IPAddrPrefix(int(masklen), ip)
|
||||
else:
|
||||
vrf_table = self._tables.get(table_id)
|
||||
if not vrf_table:
|
||||
raise BgpCoreError(desc='Vrf for route distinguisher %s does '
|
||||
'not exist.' % route_dist)
|
||||
ip6, masklen = prefix.split('/')
|
||||
prefix = IP6AddrPrefix(int(masklen), ip6)
|
||||
# We do not check if we have a path to given prefix, we issue
|
||||
# withdrawal. Hence multiple withdrawals have not side effect.
|
||||
return vrf_table.insert_vrf_path(prefix, is_withdraw=True)
|
||||
|
||||
def clean_stale_routes(self, peer, route_family=None):
|
||||
"""Removes old routes from `peer` from `route_family` table.
|
||||
|
||||
|
||||
@ -22,32 +22,22 @@ import logging
|
||||
from ryu.lib.packet.bgp import EvpnNLRI
|
||||
from ryu.lib.packet.bgp import RF_L2_EVPN
|
||||
|
||||
from ryu.services.protocols.bgp.info_base.base import Path
|
||||
from ryu.services.protocols.bgp.info_base.base import Table
|
||||
from ryu.services.protocols.bgp.info_base.base import Destination
|
||||
from ryu.services.protocols.bgp.info_base.base import NonVrfPathProcessingMixin
|
||||
from ryu.services.protocols.bgp.info_base.vpn import VpnDest
|
||||
from ryu.services.protocols.bgp.info_base.vpn import VpnPath
|
||||
from ryu.services.protocols.bgp.info_base.vpn import VpnTable
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.info_base.evpn')
|
||||
|
||||
|
||||
class EvpnDest(Destination, NonVrfPathProcessingMixin):
|
||||
class EvpnDest(VpnDest):
|
||||
"""EVPN Destination
|
||||
|
||||
Store EVPN paths.
|
||||
Store EVPN Paths.
|
||||
"""
|
||||
ROUTE_FAMILY = RF_L2_EVPN
|
||||
|
||||
def _best_path_lost(self):
|
||||
old_best_path = self._best_path
|
||||
NonVrfPathProcessingMixin._best_path_lost(self)
|
||||
self._core_service._signal_bus.best_path_changed(old_best_path, True)
|
||||
|
||||
def _new_best_path(self, best_path):
|
||||
NonVrfPathProcessingMixin._new_best_path(self, best_path)
|
||||
self._core_service._signal_bus.best_path_changed(best_path, False)
|
||||
|
||||
|
||||
class EvpnTable(Table):
|
||||
class EvpnTable(VpnTable):
|
||||
"""Global table to store EVPN routing information.
|
||||
|
||||
Uses `EvpnDest` to store destination information for each known EVPN
|
||||
@ -56,25 +46,8 @@ class EvpnTable(Table):
|
||||
ROUTE_FAMILY = RF_L2_EVPN
|
||||
VPN_DEST_CLASS = EvpnDest
|
||||
|
||||
def __init__(self, core_service, signal_bus):
|
||||
super(EvpnTable, self).__init__(None, core_service, signal_bus)
|
||||
|
||||
def _table_key(self, nlri):
|
||||
"""Return a key that will uniquely identify this NLRI inside
|
||||
this table.
|
||||
"""
|
||||
return nlri.formatted_nlri_str
|
||||
|
||||
def _create_dest(self, nlri):
|
||||
return self.VPN_DEST_CLASS(self, nlri)
|
||||
|
||||
def __str__(self):
|
||||
return '%s(scope_id: %s, rf: %s)' % (
|
||||
self.__class__.__name__, self.scope_id, self.route_family
|
||||
)
|
||||
|
||||
|
||||
class EvpnPath(Path):
|
||||
class EvpnPath(VpnPath):
|
||||
"""Represents a way of reaching an EVPN destination."""
|
||||
ROUTE_FAMILY = RF_L2_EVPN
|
||||
VRF_PATH_CLASS = None # defined in init - anti cyclic import hack
|
||||
@ -82,5 +55,5 @@ class EvpnPath(Path):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EvpnPath, self).__init__(*args, **kwargs)
|
||||
# TODO:
|
||||
# To support the VRF table for BGP EVPN routes.
|
||||
from ryu.services.protocols.bgp.info_base.vrfevpn import VrfEvpnPath
|
||||
self.VRF_PATH_CLASS = VrfEvpnPath
|
||||
|
||||
@ -21,6 +21,7 @@ import abc
|
||||
import logging
|
||||
import six
|
||||
|
||||
from ryu.lib.packet.bgp import RF_L2_EVPN
|
||||
from ryu.services.protocols.bgp.info_base.base import Destination
|
||||
from ryu.services.protocols.bgp.info_base.base import NonVrfPathProcessingMixin
|
||||
from ryu.services.protocols.bgp.info_base.base import Path
|
||||
@ -63,23 +64,30 @@ class VpnPath(Path):
|
||||
NLRI_CLASS = None
|
||||
|
||||
def clone_to_vrf(self, is_withdraw=False):
|
||||
vrf_nlri = self.NLRI_CLASS(self._nlri.prefix)
|
||||
if self.ROUTE_FAMILY == RF_L2_EVPN:
|
||||
nlri_cls = self.NLRI_CLASS._lookup_type(self._nlri.type)
|
||||
kwargs = dict(self._nlri.__dict__)
|
||||
kwargs.pop('type', None)
|
||||
vrf_nlri = nlri_cls(**kwargs)
|
||||
else: # self.ROUTE_FAMILY in [RF_IPv4_VPN, RF_IPv46_VPN]
|
||||
vrf_nlri = self.NLRI_CLASS(self._nlri.prefix)
|
||||
|
||||
pathattrs = None
|
||||
if not is_withdraw:
|
||||
pathattrs = self.pathattr_map
|
||||
|
||||
vrf_path = self.VRF_PATH_CLASS(
|
||||
self.VRF_PATH_CLASS.create_puid(
|
||||
puid=self.VRF_PATH_CLASS.create_puid(
|
||||
self._nlri.route_dist,
|
||||
self._nlri.prefix
|
||||
),
|
||||
self.source, vrf_nlri,
|
||||
self.source_version_num,
|
||||
self._nlri.prefix),
|
||||
source=self.source,
|
||||
nlri=vrf_nlri,
|
||||
src_ver_num=self.source_version_num,
|
||||
pattrs=pathattrs,
|
||||
nexthop=self.nexthop,
|
||||
is_withdraw=is_withdraw,
|
||||
label_list=self._nlri.label_list)
|
||||
|
||||
return vrf_path
|
||||
|
||||
|
||||
|
||||
@ -30,6 +30,7 @@ from ryu.lib.packet.bgp import BGPPathAttributeAsPath
|
||||
from ryu.lib.packet.bgp import BGPPathAttributeExtendedCommunities
|
||||
from ryu.lib.packet.bgp import BGPTwoOctetAsSpecificExtendedCommunity
|
||||
from ryu.lib.packet.bgp import BGPPathAttributeMultiExitDisc
|
||||
from ryu.lib.packet.bgp import RF_L2_EVPN
|
||||
|
||||
from ryu.services.protocols.bgp.base import OrderedDict
|
||||
from ryu.services.protocols.bgp.constants import VPN_TABLE
|
||||
@ -87,7 +88,10 @@ class VrfTable(Table):
|
||||
"""Return a key that will uniquely identify this NLRI inside
|
||||
this table.
|
||||
"""
|
||||
return str(nlri)
|
||||
# Note: We use `prefix` representation of the NLRI, because
|
||||
# BGP route can be identified without the route distinguisher
|
||||
# value in the VRF space.
|
||||
return nlri.prefix
|
||||
|
||||
def _create_dest(self, nlri):
|
||||
return self.VRF_DEST_CLASS(self, nlri)
|
||||
@ -134,7 +138,8 @@ class VrfTable(Table):
|
||||
self.import_vpn_path(vpn_path)
|
||||
|
||||
def import_vpn_path(self, vpn_path):
|
||||
"""Imports `vpnv(4|6)_path` into `vrf(4|6)_table`.
|
||||
"""Imports `vpnv(4|6)_path` into `vrf(4|6)_table` or `evpn_path`
|
||||
into vrfevpn_table`.
|
||||
|
||||
:Parameters:
|
||||
- `vpn_path`: (Path) VPN path that will be cloned and imported
|
||||
@ -148,17 +153,24 @@ class VrfTable(Table):
|
||||
source = vpn_path.source
|
||||
if not source:
|
||||
source = VRF_TABLE
|
||||
ip, masklen = vpn_path.nlri.prefix.split('/')
|
||||
vrf_nlri = self.NLRI_CLASS(length=int(masklen), addr=ip)
|
||||
|
||||
vpn_nlri = vpn_path.nlri
|
||||
puid = self.VRF_PATH_CLASS.create_puid(vpn_nlri.route_dist,
|
||||
vpn_nlri.prefix)
|
||||
if self.VPN_ROUTE_FAMILY == RF_L2_EVPN:
|
||||
nlri_cls = self.NLRI_CLASS._lookup_type(vpn_path.nlri.type)
|
||||
kwargs = dict(vpn_path.nlri.__dict__)
|
||||
kwargs.pop('type', None)
|
||||
vrf_nlri = nlri_cls(**kwargs)
|
||||
else: # self.VPN_ROUTE_FAMILY in [RF_IPv4_VPN, RF_IPv6_VPN]
|
||||
# Copy NLRI instance
|
||||
ip, masklen = vpn_path.nlri.prefix.split('/')
|
||||
vrf_nlri = self.NLRI_CLASS(length=int(masklen), addr=ip)
|
||||
|
||||
vrf_path = self.VRF_PATH_CLASS(
|
||||
puid,
|
||||
source,
|
||||
vrf_nlri,
|
||||
vpn_path.source_version_num,
|
||||
puid=self.VRF_PATH_CLASS.create_puid(
|
||||
vpn_path.nlri.route_dist,
|
||||
vpn_path.nlri.prefix),
|
||||
source=source,
|
||||
nlri=vrf_nlri,
|
||||
src_ver_num=vpn_path.source_version_num,
|
||||
pattrs=vpn_path.pathattr_map,
|
||||
nexthop=vpn_path.nexthop,
|
||||
is_withdraw=vpn_path.is_withdraw,
|
||||
@ -197,9 +209,9 @@ class VrfTable(Table):
|
||||
changed_dests.append(dest)
|
||||
return changed_dests
|
||||
|
||||
def insert_vrf_path(self, ip_nlri, next_hop=None,
|
||||
def insert_vrf_path(self, nlri, next_hop=None,
|
||||
gen_lbl=False, is_withdraw=False):
|
||||
assert ip_nlri
|
||||
assert nlri
|
||||
pattrs = None
|
||||
label_list = []
|
||||
vrf_conf = self.vrf_conf
|
||||
@ -253,10 +265,10 @@ class VrfTable(Table):
|
||||
label_list.append(table_manager.get_next_vpnv4_label())
|
||||
|
||||
puid = self.VRF_PATH_CLASS.create_puid(
|
||||
vrf_conf.route_dist, ip_nlri.prefix
|
||||
)
|
||||
vrf_conf.route_dist, nlri.prefix)
|
||||
|
||||
path = self.VRF_PATH_CLASS(
|
||||
puid, None, ip_nlri, 0, pattrs=pattrs,
|
||||
puid, None, nlri, 0, pattrs=pattrs,
|
||||
nexthop=next_hop, label_list=label_list,
|
||||
is_withdraw=is_withdraw
|
||||
)
|
||||
@ -410,7 +422,7 @@ class VrfDest(Destination):
|
||||
# version num. as new_paths are implicit withdrawal of old
|
||||
# paths and when doing RouteRefresh (not EnhancedRouteRefresh)
|
||||
# we get same paths again.
|
||||
if (new_path.puid == path.puid):
|
||||
if new_path.puid == path.puid:
|
||||
old_paths.append(path)
|
||||
break
|
||||
|
||||
@ -489,22 +501,30 @@ class VrfPath(Path):
|
||||
return clone
|
||||
|
||||
def clone_to_vpn(self, route_dist, for_withdrawal=False):
|
||||
ip, masklen = self._nlri.prefix.split('/')
|
||||
vpn_nlri = self.VPN_NLRI_CLASS(length=int(masklen),
|
||||
addr=ip,
|
||||
labels=self.label_list,
|
||||
route_dist=route_dist)
|
||||
if self.ROUTE_FAMILY == RF_L2_EVPN:
|
||||
nlri_cls = self.VPN_NLRI_CLASS._lookup_type(self._nlri.type)
|
||||
kwargs = dict(self._nlri.__dict__)
|
||||
kwargs.pop('type', None)
|
||||
vpn_nlri = nlri_cls(**kwargs)
|
||||
else: # self.ROUTE_FAMILY in [RF_IPv4_UC, RF_IPv6_UC]
|
||||
ip, masklen = self._nlri.prefix.split('/')
|
||||
vpn_nlri = self.VPN_NLRI_CLASS(length=int(masklen),
|
||||
addr=ip,
|
||||
labels=self.label_list,
|
||||
route_dist=route_dist)
|
||||
|
||||
pathattrs = None
|
||||
if not for_withdrawal:
|
||||
pathattrs = self.pathattr_map
|
||||
|
||||
vpnv_path = self.VPN_PATH_CLASS(
|
||||
self.source, vpn_nlri,
|
||||
self.source_version_num,
|
||||
source=self.source,
|
||||
nlri=vpn_nlri,
|
||||
src_ver_num=self.source_version_num,
|
||||
pattrs=pathattrs,
|
||||
nexthop=self.nexthop,
|
||||
is_withdraw=for_withdrawal
|
||||
)
|
||||
is_withdraw=for_withdrawal)
|
||||
|
||||
return vpnv_path
|
||||
|
||||
def __eq__(self, b_path):
|
||||
|
||||
58
ryu/services/protocols/bgp/info_base/vrfevpn.py
Normal file
58
ryu/services/protocols/bgp/info_base/vrfevpn.py
Normal file
@ -0,0 +1,58 @@
|
||||
# Copyright (C) 2016 Nippon Telegraph and Telephone Corporation.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
Defines data types and models required specifically for VRF (for EVPN)
|
||||
support. Represents data structures for VRF not VPN/global.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ryu.lib.packet.bgp import RF_L2_EVPN
|
||||
from ryu.lib.packet.bgp import EvpnNLRI
|
||||
|
||||
from ryu.services.protocols.bgp.info_base.evpn import EvpnPath
|
||||
from ryu.services.protocols.bgp.info_base.vrf import VrfDest
|
||||
from ryu.services.protocols.bgp.info_base.vrf import VrfNlriImportMap
|
||||
from ryu.services.protocols.bgp.info_base.vrf import VrfPath
|
||||
from ryu.services.protocols.bgp.info_base.vrf import VrfTable
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.info_base.vrfevpn')
|
||||
|
||||
|
||||
class VrfEvpnPath(VrfPath):
|
||||
"""Represents a way of reaching an EVPN destination with a VPN."""
|
||||
ROUTE_FAMILY = RF_L2_EVPN
|
||||
VPN_PATH_CLASS = EvpnPath
|
||||
VPN_NLRI_CLASS = EvpnNLRI
|
||||
|
||||
|
||||
class VrfEvpnDest(VrfDest):
|
||||
"""Destination for EVPN VRFs."""
|
||||
ROUTE_FAMILY = RF_L2_EVPN
|
||||
|
||||
|
||||
class VrfEvpnTable(VrfTable):
|
||||
"""Virtual Routing and Forwarding information base for EVPN."""
|
||||
ROUTE_FAMILY = RF_L2_EVPN
|
||||
VPN_ROUTE_FAMILY = RF_L2_EVPN
|
||||
NLRI_CLASS = EvpnNLRI
|
||||
VRF_PATH_CLASS = VrfEvpnPath
|
||||
VRF_DEST_CLASS = VrfEvpnDest
|
||||
|
||||
|
||||
class VrfEvpnNlriImportMap(VrfNlriImportMap):
|
||||
VRF_PATH_CLASS = VrfEvpnPath
|
||||
NLRI_CLASS = EvpnNLRI
|
||||
@ -15,10 +15,12 @@ from .route_formatter_mixin import RouteFormatterMixin
|
||||
|
||||
LOG = logging.getLogger('bgpspeaker.operator.commands.show.vrf')
|
||||
|
||||
SUPPORTED_VRF_RF = ('ipv4', 'ipv6', 'evpn')
|
||||
|
||||
|
||||
class Routes(Command, RouteFormatterMixin):
|
||||
help_msg = 'show routes present for vrf'
|
||||
param_help_msg = '<vpn-name> <route-family>(ipv4, ipv6)'
|
||||
param_help_msg = '<vpn-name> <route-family>%s' % str(SUPPORTED_VRF_RF)
|
||||
command = 'routes'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -32,8 +34,9 @@ class Routes(Command, RouteFormatterMixin):
|
||||
return WrongParamResp()
|
||||
vrf_name = params[0]
|
||||
vrf_rf = params[1]
|
||||
if vrf_rf not in ('ipv4', 'ipv6'):
|
||||
return WrongParamResp('route-family not one of (ipv4, ipv6)')
|
||||
if vrf_rf not in SUPPORTED_VRF_RF:
|
||||
return WrongParamResp('route-family not one of %s' %
|
||||
str(SUPPORTED_VRF_RF))
|
||||
|
||||
from ryu.services.protocols.bgp.operator.internal_api import \
|
||||
WrongParamError
|
||||
|
||||
@ -180,7 +180,7 @@ class InternalApi(object):
|
||||
route_families.extend(SUPPORTED_GLOBAL_RF)
|
||||
else:
|
||||
route_family = RouteFamily(afi, safi)
|
||||
if (route_family not in SUPPORTED_GLOBAL_RF):
|
||||
if route_family not in SUPPORTED_GLOBAL_RF:
|
||||
raise WrongParamError('Not supported address-family'
|
||||
' %s, %s' % (afi, safi))
|
||||
route_families.append(route_family)
|
||||
|
||||
@ -22,6 +22,7 @@ import logging
|
||||
|
||||
from ryu.lib.packet.bgp import RF_IPv4_UC
|
||||
from ryu.lib.packet.bgp import RF_IPv6_UC
|
||||
from ryu.lib.packet.bgp import RF_L2_EVPN
|
||||
|
||||
from ryu.services.protocols.bgp.utils import validation
|
||||
from ryu.services.protocols.bgp.base import get_validator
|
||||
@ -54,10 +55,11 @@ VRF_DESC = 'vrf_desc'
|
||||
VRF_RF = 'route_family'
|
||||
IMPORT_MAPS = 'import_maps'
|
||||
|
||||
# Two supported VRF route-families
|
||||
VRF_RF_IPV6 = 'ipv6'
|
||||
# Supported VRF route-families
|
||||
VRF_RF_IPV4 = 'ipv4'
|
||||
SUPPORTED_VRF_RF = (VRF_RF_IPV4, VRF_RF_IPV6)
|
||||
VRF_RF_IPV6 = 'ipv6'
|
||||
VRF_RF_L2_EVPN = 'evpn'
|
||||
SUPPORTED_VRF_RF = (VRF_RF_IPV4, VRF_RF_IPV6, VRF_RF_L2_EVPN)
|
||||
|
||||
|
||||
# Default configuration values.
|
||||
@ -77,8 +79,7 @@ def validate_import_rts(import_rts):
|
||||
# Check if we have duplicates
|
||||
unique_rts = set(import_rts)
|
||||
if len(unique_rts) != len(import_rts):
|
||||
raise ConfigValueError(desc='Duplicate value provided %s' %
|
||||
(import_rts))
|
||||
raise ConfigValueError(desc='Duplicate value provided %s' % import_rts)
|
||||
|
||||
return import_rts
|
||||
|
||||
@ -97,7 +98,7 @@ def validate_export_rts(export_rts):
|
||||
unique_rts = set(export_rts)
|
||||
if len(unique_rts) != len(export_rts):
|
||||
raise ConfigValueError(desc='Duplicate value provided in %s' %
|
||||
(export_rts))
|
||||
export_rts)
|
||||
return export_rts
|
||||
|
||||
|
||||
@ -223,6 +224,8 @@ class VrfConf(ConfWithId, ConfWithStats):
|
||||
return RF_IPv4_UC
|
||||
elif vrf_rf == VRF_RF_IPV6:
|
||||
return RF_IPv6_UC
|
||||
elif vrf_rf == VRF_RF_L2_EVPN:
|
||||
return RF_L2_EVPN
|
||||
else:
|
||||
raise ValueError('Unsupported VRF route family given %s' % vrf_rf)
|
||||
|
||||
@ -232,6 +235,8 @@ class VrfConf(ConfWithId, ConfWithStats):
|
||||
return VRF_RF_IPV4
|
||||
elif route_family == RF_IPv6_UC:
|
||||
return VRF_RF_IPV6
|
||||
elif route_family == RF_L2_EVPN:
|
||||
return VRF_RF_L2_EVPN
|
||||
else:
|
||||
raise ValueError('No supported mapping for route family '
|
||||
'to vrf_route_family exists for %s' %
|
||||
@ -322,7 +327,7 @@ class VrfConf(ConfWithId, ConfWithStats):
|
||||
|
||||
import_rts = set(import_rts)
|
||||
if not import_rts.symmetric_difference(curr_import_rts):
|
||||
return (None, None)
|
||||
return None, None
|
||||
|
||||
# Get the difference between current and new RTs
|
||||
new_import_rts = import_rts - curr_import_rts
|
||||
@ -330,7 +335,7 @@ class VrfConf(ConfWithId, ConfWithStats):
|
||||
|
||||
# Update current RTs and notify listeners.
|
||||
self._settings[IMPORT_RTS] = import_rts
|
||||
return (new_import_rts, old_import_rts)
|
||||
return new_import_rts, old_import_rts
|
||||
|
||||
def _update_export_rts(self, **kwargs):
|
||||
export_rts = kwargs.get(EXPORT_RTS)
|
||||
@ -381,7 +386,7 @@ class VrfConf(ConfWithId, ConfWithStats):
|
||||
self.export_rts, self.soo_list))
|
||||
|
||||
def __str__(self):
|
||||
return ('VrfConf-%s' % (self.route_dist))
|
||||
return 'VrfConf-%s' % self.route_dist
|
||||
|
||||
|
||||
class VrfsConf(BaseConf):
|
||||
@ -451,7 +456,7 @@ class VrfsConf(BaseConf):
|
||||
vrf_rfs = SUPPORTED_VRF_RF
|
||||
# If asked to delete specific route family vrf conf.
|
||||
if vrf_rf:
|
||||
vrf_rfs = (vrf_rf)
|
||||
vrf_rfs = vrf_rf
|
||||
|
||||
# For all vrf route family asked to be deleted, we collect all deleted
|
||||
# VrfConfs
|
||||
@ -478,7 +483,6 @@ class VrfsConf(BaseConf):
|
||||
if route_dist is None and vrf_id is None:
|
||||
raise RuntimeConfigError(desc='To get VRF supply route_dist '
|
||||
'or vrf_id.')
|
||||
vrf = None
|
||||
if route_dist is not None and vrf_id is not None:
|
||||
vrf1 = self._vrfs_by_id.get(vrf_id)
|
||||
rd_rf_id = VrfConf.create_rd_rf_id(route_dist, vrf_rf)
|
||||
@ -500,8 +504,8 @@ class VrfsConf(BaseConf):
|
||||
return dict(self._vrfs_by_rd_rf)
|
||||
|
||||
@classmethod
|
||||
def get_valid_evts(self):
|
||||
self_valid_evts = super(VrfsConf, self).get_valid_evts()
|
||||
def get_valid_evts(cls):
|
||||
self_valid_evts = super(VrfsConf, cls).get_valid_evts()
|
||||
self_valid_evts.update(VrfsConf.VALID_EVT)
|
||||
return self_valid_evts
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user