From e3afbb0abaca326f65d6a598a9e09321dda1ca67 Mon Sep 17 00:00:00 2001 From: Luis Tomas Bolivar Date: Mon, 30 Aug 2021 13:27:32 +0200 Subject: [PATCH] Add initial support for EVPN Change-Id: I8c6ffc192158b96ea3186501ae6579bd2934d37b --- README.rst | 2 +- doc/source/contributor/evpn_mode_design.rst | 8 +- ovn_bgp_agent/constants.py | 7 + .../drivers/openstack/ovn_bgp_driver.py | 4 +- .../drivers/openstack/ovn_evpn_driver.py | 758 ++++++++++++++++++ ovn_bgp_agent/drivers/openstack/utils/ovn.py | 6 +- .../drivers/openstack/watchers/bgp_watcher.py | 39 +- .../openstack/watchers/evpn_watcher.py | 246 ++++++ setup.cfg | 3 +- 9 files changed, 1043 insertions(+), 30 deletions(-) create mode 100644 ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py create mode 100644 ovn_bgp_agent/drivers/openstack/watchers/evpn_watcher.py diff --git a/README.rst b/README.rst index 9172335c..67694ca7 100644 --- a/README.rst +++ b/README.rst @@ -14,4 +14,4 @@ Features * Expose VMs with FIPs or on Provider Networks through BGP on OVN environments. - +* Expose VMs on Tenant Networks through EVPN on OVN environments. diff --git a/doc/source/contributor/evpn_mode_design.rst b/doc/source/contributor/evpn_mode_design.rst index 72d000e6..c6f16c9d 100644 --- a/doc/source/contributor/evpn_mode_design.rst +++ b/doc/source/contributor/evpn_mode_design.rst @@ -40,10 +40,10 @@ the OVN overlay. This simple design allows the agent to implement different drivers, depending on what OVN SB DB events are being watched (watchers examples at -``networking_bgp_onn/drivers/openstack/watchers/``), and what actions are +``ovn_bgp_agent/drivers/openstack/watchers/``), and what actions are triggered in reaction to them (drivers examples at -``networking_bgp_ovn/drivers/openstack/XXXX_driver.py``, implementing the -``networking_bgp_von/drivers/driver_api.py``). +``ovn_bgp_agent/drivers/openstack/XXXX_driver.py``, implementing the +``ovn_bgp_agent/drivers/driver_api.py``). A new driver implements the support for EVPN capabilities with multitenancy (overlapping CIDRs), by leveraging VRFs and EVPN Type-5 Routes. The API used @@ -55,7 +55,7 @@ react to the information being added by it into the OVN SB DB (using the Proposed Solution ----------------- -To support EVPN the functionality of the ``networking-bgp-ovn`` agent needs +To support EVPN the functionality of the ``ovn_bgp_agent`` needs to be extended with a new driver that performs the extra steps required for the EVPN configuration and steering the traffic to/from the node from/to the OVN overlay. The only configuration needed is to enable the diff --git a/ovn_bgp_agent/constants.py b/ovn_bgp_agent/constants.py index 04f0c6b1..f76ad81b 100644 --- a/ovn_bgp_agent/constants.py +++ b/ovn_bgp_agent/constants.py @@ -33,7 +33,14 @@ IP_VERSION_6 = 6 IP_VERSION_4 = 4 BGP_MODE = 'BGP' +EVPN_MODE = 'EVPN' +OVN_EVPN_VNI_EXT_ID_KEY = 'neutron_bgpvpn:vni' +OVN_EVPN_AS_EXT_ID_KEY = 'neutron_bgpvpn:as' +OVN_EVPN_VRF_PREFIX = "vrf-" +OVN_EVPN_BRIDGE_PREFIX = "br-" +OVN_EVPN_VXLAN_PREFIX = "vxlan-" +OVN_EVPN_LO_PREFIX = "lo-" OVN_INTEGRATION_BRIDGE = 'br-int' LINK_UP = "up" diff --git a/ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py b/ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py index 9be5fbf2..54db6a5a 100644 --- a/ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py +++ b/ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py @@ -37,7 +37,7 @@ LOG = logging.getLogger(__name__) OVN_TABLES = ("Port_Binding", "Chassis", "Datapath_Binding", "Chassis_Private") -class OSPOVNBGPDriver(driver_api.AgentDriverBase): +class OVNBGPDriver(driver_api.AgentDriverBase): def __init__(self): self._expose_tenant_networks = CONF.expose_tenant_networks @@ -52,7 +52,7 @@ class OSPOVNBGPDriver(driver_api.AgentDriverBase): self.ovs_idl.start(constants.OVS_CONNECTION_STRING) self.chassis = self.ovs_idl.get_own_chassis_name() self.ovn_remote = self.ovs_idl.get_ovn_remote() - LOG.debug("Loaded chassis {}.".format(self.chassis)) + LOG.debug("Loaded chassis %s.", self.chassis) events = () for event in self._get_events(): diff --git a/ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py b/ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py new file mode 100644 index 00000000..33f582a6 --- /dev/null +++ b/ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py @@ -0,0 +1,758 @@ +# Copyright 2021 Red Hat, Inc. +# +# 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. + +import collections +import ipaddress + +from oslo_concurrency import lockutils +from oslo_config import cfg +from oslo_log import log as logging + +from ovn_bgp_agent import constants +from ovn_bgp_agent.drivers import driver_api +from ovn_bgp_agent.drivers.openstack.utils import frr +from ovn_bgp_agent.drivers.openstack.utils import ovn +from ovn_bgp_agent.drivers.openstack.utils import ovs +from ovn_bgp_agent.drivers.openstack.watchers import evpn_watcher as \ + watcher +from ovn_bgp_agent.utils import linux_net + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) +# LOG.setLevel(logging.DEBUG) +# logging.basicConfig(level=logging.DEBUG) + +OVN_TABLES = ("Port_Binding", "Chassis", "Datapath_Binding", "Chassis_Private") + + +class OVNEVPNDriver(driver_api.AgentDriverBase): + + def __init__(self): + self.ovn_bridge_mappings = {} # {'public': 'br-ex'} + self.ovn_local_cr_lrps = {} + self.ovn_local_lrps = {} + # {'br-ex': [route1, route2]} + self._ovn_routing_tables_routes = collections.defaultdict() + self._ovn_exposed_evpn_ips = collections.defaultdict() + + self.ovs_idl = ovs.OvsIdl() + self.ovs_idl.start(constants.OVS_CONNECTION_STRING) + self.chassis = self.ovs_idl.get_own_chassis_name() + self.ovn_remote = self.ovs_idl.get_ovn_remote() + LOG.debug("Loaded chassis %s.", self.chassis) + + events = () + for event in self._get_events(): + event_class = getattr(watcher, event) + events += (event_class(self),) + + self._sb_idl = ovn.OvnSbIdl( + self.ovn_remote, + chassis=self.chassis, + tables=OVN_TABLES, + events=events) + + def start(self): + # start the subscriptions to the OSP events. This ensures the watcher + # calls the relevant driver methods upon registered events + self.sb_idl = self._sb_idl.start() + + def _get_events(self): + events = set(["PortBindingChassisCreatedEvent", + "PortBindingChassisDeletedEvent", + "SubnetRouterAttachedEvent", + "SubnetRouterDetachedEvent", + "TenantPortCreatedEvent", + "TenantPortDeletedEvent", + "ChassisCreateEvent"]) + return events + + @lockutils.synchronized('evpn') + def sync(self): + self.ovn_local_cr_lrps = {} + self.ovn_local_lrps = {} + self._ovn_routing_tables_routes = collections.defaultdict() + self._ovn_exposed_evpn_ips = collections.defaultdict() + + # 1) Get bridge mappings: xxxx:br-ex,yyyy:br-ex2 + bridge_mappings = self.ovs_idl.get_ovn_bridge_mappings() + # 2) Get macs for bridge mappings + for bridge_mapping in bridge_mappings: + network = bridge_mapping.split(":")[0] + bridge = bridge_mapping.split(":")[1] + self.ovn_bridge_mappings[network] = bridge + + # TO DO + # add missing routes/ips for fips/provider VMs + ports = self.sb_idl.get_ports_on_chassis(self.chassis) + for port in ports: + if port.type != constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE: + continue + self._expose_ip(port, cr_lrp=True) + + self._remove_extra_exposed_ips() + self._remove_extra_routes() + self._remove_extra_ovs_flows() + self._remove_extra_vrfs() + + def _ensure_network_exposed(self, router_port, gateway): + evpn_info = self.sb_idl.get_evpn_info_from_lrp_port_name( + router_port.logical_port) + if not evpn_info: + LOG.debug("No EVPN information for LRP Port %s. " + "Not exposing it.", router_port) + return + + gateway_ips = [ip.split('/')[0] for ip in gateway['ips']] + try: + router_port_ip = router_port.mac[0].split(' ')[1] + except IndexError: + return + router_ip = router_port_ip.split('/')[0] + if router_ip in gateway_ips: + return + self.ovn_local_lrps[router_port.logical_port] = { + 'datapath': router_port.datapath, + 'ip': router_port_ip + } + datapath_bridge, vlan_tag = self._get_bridge_for_datapath( + gateway['provider_datapath']) + + router_port_ip_version = linux_net.get_ip_version(router_port_ip) + for gateway_ip in gateway_ips: + if linux_net.get_ip_version(gateway_ip) == router_port_ip_version: + linux_net.add_ip_route( + self._ovn_routing_tables_routes, + router_ip, + gateway['vni'], + datapath_bridge, + vlan=vlan_tag, + mask=router_port_ip.split("/")[1], + via=gateway_ip) + break + + if router_port_ip_version == constants.IP_VERSION_6: + net_ip = '{}'.format(ipaddress.IPv6Network( + router_port_ip, strict=False)) + else: + net_ip = '{}'.format(ipaddress.IPv4Network( + router_port_ip, strict=False)) + + strip_vlan = False + if vlan_tag: + strip_vlan = True + ovs.ensure_evpn_ovs_flow(datapath_bridge, + constants.OVS_VRF_RULE_COOKIE, + gateway['mac'], + gateway['vrf'], + net_ip, + strip_vlan) + + network_port_datapath = self.sb_idl.get_port_datapath( + router_port.options['peer']) + if not network_port_datapath: + return + ports = self.sb_idl.get_ports_on_datapath( + network_port_datapath) + for port in ports: + if (port.type not in (constants.OVN_VM_VIF_PORT_TYPE, + constants.OVN_VIRTUAL_VIF_PORT_TYPE) or + (port.type == constants.OVN_VM_VIF_PORT_TYPE and + not port.chassis)): + continue + try: + port_ips = [port.mac[0].split(' ')[1]] + except IndexError: + continue + if len(port.mac[0].split(' ')) == 3: + port_ips.append(port.mac[0].split(' ')[2]) + + for port_ip in port_ips: + # Only adding the port ips that match the lrp + # IP version + port_ip_version = linux_net.get_ip_version(port_ip) + if port_ip_version == router_port_ip_version: + linux_net.add_ips_to_dev( + gateway['lo'], [port_ip], + clear_local_route_at_table=gateway['vni']) + self._ovn_exposed_evpn_ips.setdefault( + gateway['lo'], []).extend([port_ip]) + + def _get_bridge_for_datapath(self, datapath): + network_name, network_tag = self.sb_idl.get_network_name_and_tag( + datapath, self.ovn_bridge_mappings.keys()) + if network_name: + if network_tag: + return self.ovn_bridge_mappings[network_name], network_tag[0] + return self.ovn_bridge_mappings[network_name], None + return None, None + + @lockutils.synchronized('evpn') + def expose_ip(self, row, cr_lrp=False): + '''Advertice BGP route through EVPN. + + This methods ensures BGP advertises the IP through the required + VRF/Tenant by using the specified VNI/VXLAN id. + + It relies on Zebra, which creates and advertises a route when an IP + is added to a interface in the related VRF. + ''' + self._expose_ip(row, cr_lrp) + + def _expose_ip(self, row, cr_lrp=False): + if cr_lrp: + cr_lrp_port_name = row.logical_port + cr_lrp_port = row + else: + cr_lrp_port_name = 'cr-lrp-' + row.logical_port + cr_lrp_port = self.sb_idl.get_port_if_local_chassis( + cr_lrp_port_name, self.chassis) + if not cr_lrp_port: + # Not in local chassis, no need to proccess + return + + _, cr_lrp_datapath = self.sb_idl.get_fip_associated( + cr_lrp_port_name) + if not cr_lrp_datapath: + return + + if (len(cr_lrp_port.mac[0].split(' ')) != 2 and + len(cr_lrp_port.mac[0].split(' ')) != 3): + return + ips = [cr_lrp_port.mac[0].split(' ')[1]] + # for dual-stack + if len(cr_lrp_port.mac[0].split(' ')) == 3: + ips.append(cr_lrp_port.mac[0].split(' ')[2]) + + if cr_lrp: + evpn_info = self.sb_idl.get_evpn_info_from_crlrp_port_name( + cr_lrp_port_name) + else: + evpn_info = self.sb_idl.get_evpn_info_from_port(row) + if not evpn_info: + LOG.debug("No EVPN information for CR-LRP Port with IPs %s. " + "Not exposing it.", ips) + return + + LOG.info("Adding BGP route for CR-LRP Port %s on AS %s and " + "VNI %s", ips, evpn_info['bgp_as'], evpn_info['vni']) + vrf, lo, bridge, vxlan = self._ensure_evpn_devices(evpn_info['vni']) + if not vrf or not lo: + return + + self.ovn_local_cr_lrps[cr_lrp_port_name] = { + 'router_datapath': cr_lrp_port.datapath, + 'provider_datapath': cr_lrp_datapath, + 'ips': ips, + 'mac': cr_lrp_port.mac[0].split(' ')[0], + 'vni': int(evpn_info['vni']), + 'bgp_as': evpn_info['bgp_as'], + 'lo': lo, + 'bridge': bridge, + 'vxlan': vxlan, + 'vrf': vrf + } + + frr.vrf_reconfigure(evpn_info, action="add-vrf") + + datapath_bridge, vlan_tag = self._get_bridge_for_datapath( + cr_lrp_datapath) + + ips_without_mask = [ip.split("/")[0] for ip in ips] + linux_net.add_ips_to_dev(lo, ips_without_mask) + self._ovn_exposed_evpn_ips.setdefault( + lo, []).extend(ips_without_mask) + + self._connect_evpn_to_ovn(vrf, ips, datapath_bridge, evpn_info['vni'], + vlan_tag) + + # Check if there are networks attached to the router, + # and if so, add the needed routes/rules + lrp_ports = self.sb_idl.get_lrp_ports_for_router( + cr_lrp_port.datapath) + for lrp in lrp_ports: + if lrp.chassis: + continue + self._ensure_network_exposed( + lrp, self.ovn_local_cr_lrps[cr_lrp_port_name]) + + @lockutils.synchronized('evpn') + def withdraw_ip(self, row, cr_lrp=False): + '''Withdraw BGP route through EVPN. + + This methods ensures BGP withdraw the IP advertised through the + required VRF/Tenant by using the specified VNI/VXLAN id. + + It relies on Zebra, which cwithdraws the advertisement as son as the + IP is deleted from the interface in the related VRF. + ''' + if cr_lrp: + cr_lrp_port_name = row.logical_port + else: + cr_lrp_port_name = 'cr-lrp-' + row.logical_port + + cr_lrp_info = self.ovn_local_cr_lrps.get(cr_lrp_port_name, {}) + if not cr_lrp_info: + # This means it is in a different chassis + return + cr_lrp_datapath = cr_lrp_info.get('provider_datapath') + if not cr_lrp_datapath: + return + + ips = cr_lrp_info.get('ips') + evpn_vni = cr_lrp_info.get('vni') + if not evpn_vni: + LOG.debug("No EVPN information for CR-LRP Port with IPs %s. " + "No need to withdraw it.", ips) + return + + LOG.info("Delete BGP route for CR-LRP Port %s on VNI %s", ips, + evpn_vni) + datapath_bridge, vlan_tag = self._get_bridge_for_datapath( + cr_lrp_datapath) + + if vlan_tag: + self._disconnect_evpn_to_ovn(evpn_vni, datapath_bridge, ips, + vlan_tag=vlan_tag) + else: + cr_lrps_on_same_provider = [ + p for p in self.ovn_local_cr_lrps.values() + if p['provider_datapath'] == cr_lrp_datapath] + if (len(cr_lrps_on_same_provider) > 1): + # NOTE: no need to remove the NDP proxy if there are other + # cr-lrp ports on the same chassis connected to the same + # provider flat network + self._disconnect_evpn_to_ovn(evpn_vni, datapath_bridge, ips, + cleanup_ndp_proxy=False) + else: + self._disconnect_evpn_to_ovn(evpn_vni, datapath_bridge, ips) + + self._remove_evpn_devices(evpn_vni) + ovs.remove_evpn_router_ovs_flows(datapath_bridge, + constants.OVS_VRF_RULE_COOKIE, + cr_lrp_info.get('mac')) + + evpn_info = {'vni': evpn_vni, 'bgp_as': cr_lrp_info.get('bgp_as')} + frr.vrf_reconfigure(evpn_info, action="del-vrf") + + try: + del self.ovn_local_cr_lrps[cr_lrp_port_name] + except KeyError: + LOG.debug("Gateway port already cleanup from the agent: %s", + cr_lrp_port_name) + + @lockutils.synchronized('evpn') + def expose_remote_ip(self, ips, row): + if self.sb_idl.is_provider_network(row.datapath): + return + port_lrp = self.sb_idl.get_lrp_port_for_datapath(row.datapath) + if port_lrp in self.ovn_local_lrps.keys(): + evpn_info = self.sb_idl.get_evpn_info_from_lrp_port_name(port_lrp) + if not evpn_info: + LOG.debug("No EVPN information for LRP Port %s. " + "Not exposing IPs: %s.", port_lrp, ips) + return + LOG.info("Add BGP route for tenant IP %s on chassis %s", + ips, self.chassis) + lo_name = constants.OVN_EVPN_LO_PREFIX + str(evpn_info['vni']) + linux_net.add_ips_to_dev( + lo_name, ips, clear_local_route_at_table=evpn_info['vni']) + self._ovn_exposed_evpn_ips.setdefault( + lo_name, []).extend(ips) + + @lockutils.synchronized('evpn') + def withdraw_remote_ip(self, ips, row): + if self.sb_idl.is_provider_network(row.datapath): + return + port_lrp = self.sb_idl.get_lrp_port_for_datapath(row.datapath) + if port_lrp in self.ovn_local_lrps.keys(): + evpn_info = self.sb_idl.get_evpn_info_from_lrp_port_name(port_lrp) + if not evpn_info: + LOG.debug("No EVPN information for LRP Port %s. " + "Not withdrawing IPs: %s.", port_lrp, ips) + return + LOG.info("Delete BGP route for tenant IP %s on chassis %s", + ips, self.chassis) + lo_name = constants.OVN_EVPN_LO_PREFIX + str(evpn_info['vni']) + linux_net.del_ips_from_dev(lo_name, ips) + + @lockutils.synchronized('evpn') + def expose_subnet(self, row): + evpn_info = self.sb_idl.get_evpn_info_from_port(row) + ip = self.sb_idl.get_ip_from_port_peer(row) + if not evpn_info: + LOG.debug("No EVPN information for LRP Port %s. " + "Not exposing IPs: %s.", row.logical_port, ip) + return + + lrp_logical_port = 'lrp-' + row.logical_port + lrp_datapath = self.sb_idl.get_port_datapath(lrp_logical_port) + + cr_lrp = self.sb_idl.is_router_gateway_on_chassis(lrp_datapath, + self.chassis) + if not cr_lrp: + return + + LOG.info("Add IP Routes for network %s on chassis %s", ip, + self.chassis) + self.ovn_local_lrps[lrp_logical_port] = { + 'datapath': lrp_datapath, + 'ip': ip + } + + cr_lrp_info = self.ovn_local_cr_lrps.get(cr_lrp, {}) + cr_lrp_datapath = cr_lrp_info.get('provider_datapath') + if not cr_lrp_datapath: + LOG.info("Subnet not connected to the provider network. " + "No need to expose it through EVPN") + return + if (evpn_info['bgp_as'] != cr_lrp_info.get('bgp_as') or + evpn_info['vni'] != cr_lrp_info.get('vni')): + LOG.error("EVPN information at router port (vni: %s, as: %s) does" + " not match with information at subnet gateway port:" + " %s", cr_lrp_info.get('vni'), + cr_lrp_info.get('bgp_as'), evpn_info) + return + + cr_lrp_ips = [ip_address.split('/')[0] + for ip_address in cr_lrp_info.get('ips', [])] + datapath_bridge, vlan_tag = self._get_bridge_for_datapath( + cr_lrp_datapath) + + ip_version = linux_net.get_ip_version(ip) + for cr_lrp_ip in cr_lrp_ips: + if linux_net.get_ip_version(cr_lrp_ip) == ip_version: + linux_net.add_ip_route( + self._ovn_routing_tables_routes, + ip.split("/")[0], + evpn_info['vni'], + datapath_bridge, + vlan=vlan_tag, + mask=ip.split("/")[1], + via=cr_lrp_ip) + break + + if ip_version == constants.IP_VERSION_6: + net_ip = '{}'.format(ipaddress.IPv6Network( + ip, strict=False)) + else: + net_ip = '{}'.format(ipaddress.IPv4Network( + ip, strict=False)) + + strip_vlan = False + if vlan_tag: + strip_vlan = True + ovs.ensure_evpn_ovs_flow(datapath_bridge, + constants.OVS_VRF_RULE_COOKIE, + cr_lrp_info['mac'], + cr_lrp_info['vrf'], + net_ip, + strip_vlan) + + # Check if there are VMs on the network + # and if so expose the route + network_port_datapath = row.datapath + if not network_port_datapath: + return + ports = self.sb_idl.get_ports_on_datapath( + network_port_datapath) + for port in ports: + if (port.type not in (constants.OVN_VM_VIF_PORT_TYPE, + constants.OVN_VIRTUAL_VIF_PORT_TYPE)): + continue + try: + port_ips = [port.mac[0].split(' ')[1]] + except IndexError: + continue + if len(port.mac[0].split(' ')) == 3: + port_ips.append(port.mac[0].split(' ')[2]) + + for port_ip in port_ips: + # Only adding the port ips that match the lrp + # IP version + port_ip_version = linux_net.get_ip_version(port_ip) + if port_ip_version == ip_version: + linux_net.add_ips_to_dev( + cr_lrp_info['lo'], [port_ip], + clear_local_route_at_table=evpn_info['vni']) + self._ovn_exposed_evpn_ips.setdefault( + cr_lrp_info['lo'], []).extend([port_ip]) + + @lockutils.synchronized('evpn') + def withdraw_subnet(self, row): + lrp_logical_port = 'lrp-' + row.logical_port + lrp_datapath = self.ovn_local_lrps.get(lrp_logical_port, {}).get( + 'datapath') + ip = self.ovn_local_lrps.get(lrp_logical_port, {}).get('ip') + if not lrp_datapath: + return + + cr_lrp = self.sb_idl.is_router_gateway_on_chassis(lrp_datapath, + self.chassis) + if not cr_lrp: + return + + LOG.info("Delete IP Routes for network %s on chassis %s", ip, + self.chassis) + + cr_lrp_info = self.ovn_local_cr_lrps.get(cr_lrp, {}) + cr_lrp_datapath = cr_lrp_info.get('provider_datapath') + if not cr_lrp_datapath: + LOG.info("Subnet not connected to the provider network. " + "No need to withdraw it from EVPN") + return + cr_lrp_ips = [ip_address.split('/')[0] + for ip_address in cr_lrp_info.get('ips', [])] + datapath_bridge, vlan_tag = self._get_bridge_for_datapath( + cr_lrp_datapath) + + ip_version = linux_net.get_ip_version(ip) + for cr_lrp_ip in cr_lrp_ips: + if linux_net.get_ip_version(cr_lrp_ip) == ip_version: + linux_net.del_ip_route( + self._ovn_routing_tables_routes, + ip.split("/")[0], + cr_lrp_info['vni'], + datapath_bridge, + vlan=vlan_tag, + mask=ip.split("/")[1], + via=cr_lrp_ip) + if (linux_net.get_ip_version(cr_lrp_ip) == + constants.IP_VERSION_6): + net = ipaddress.IPv6Network(ip, strict=False) + else: + net = ipaddress.IPv4Network(ip, strict=False) + break + + ovs.remove_evpn_network_ovs_flow(datapath_bridge, + constants.OVS_VRF_RULE_COOKIE, + cr_lrp_info['mac'], + '{}'.format(net)) + + # Check if there are VMs on the network + # and if so withdraw the routes + vms_on_net = linux_net.get_exposed_ips_on_network( + cr_lrp_info['lo'], net) + linux_net.delete_exposed_ips(vms_on_net, + cr_lrp_info['lo']) + + try: + del self.ovn_local_lrps[lrp_logical_port] + except KeyError: + LOG.debug("Router Interface port already cleanup from the agent " + "%s", lrp_logical_port) + + def _ensure_evpn_devices(self, vni): + # ensure vrf device. + # NOTE: It uses vni id as table number + vrf_name = constants.OVN_EVPN_VRF_PREFIX + str(vni) + linux_net.ensure_vrf(vrf_name, vni) + + # ensure bridge device + bridge_name = constants.OVN_EVPN_BRIDGE_PREFIX + str(vni) + linux_net.ensure_bridge(bridge_name) + # connect bridge to vrf + linux_net.set_master_for_device(bridge_name, vrf_name) + + # ensure vxlan device + vxlan_name = constants.OVN_EVPN_VXLAN_PREFIX + str(vni) + # NOTE: assuming only 1 IP on the loopback device with /32 prefix + lo_ip = linux_net.get_nic_ip('lo', + ip_version=constants.IP_VERSION_4)[0] + if not lo_ip: + LOG.error("Loopback IP must have a /32 IP associated for the " + "EVPN local ip") + return None, None + linux_net.ensure_vxlan(vxlan_name, vni, lo_ip) + # connect vxlan to bridge + linux_net.set_master_for_device(vxlan_name, bridge_name) + + # ensure dummy lo interface + lo_name = constants.OVN_EVPN_LO_PREFIX + str(vni) + linux_net.ensure_dummy_device(lo_name) + # connect dummy to vrf + linux_net.set_master_for_device(lo_name, vrf_name) + + return vrf_name, lo_name, bridge_name, vxlan_name + + def _remove_evpn_devices(self, vni): + vrf_name = constants.OVN_EVPN_VRF_PREFIX + str(vni) + bridge_name = constants.OVN_EVPN_BRIDGE_PREFIX + str(vni) + vxlan_name = constants.OVN_EVPN_VXLAN_PREFIX + str(vni) + lo_name = constants.OVN_EVPN_LO_PREFIX + str(vni) + + for device in [lo_name, vrf_name, bridge_name, vxlan_name]: + linux_net.delete_device(device) + + def _connect_evpn_to_ovn(self, vrf, ips, datapath_bridge, vni, vlan_tag): + # add vrf to ovs bridge + ovs.add_device_to_ovs_bridge(vrf, datapath_bridge, vlan_tag) + + if vlan_tag: + linux_net.ensure_vlan_device_for_network(datapath_bridge, vlan_tag) + # add route for ip to ovs provider bridge (at the vrf routing table) + for ip in ips: + ip_without_mask = ip.split("/")[0] + linux_net.add_ip_route( + self._ovn_routing_tables_routes, ip_without_mask, + vni, datapath_bridge, vlan=vlan_tag) + + # add proxy ndp config for ipv6 + if (linux_net.get_ip_version(ip_without_mask) == + constants.IP_VERSION_6): + linux_net.add_ndp_proxy(ip, datapath_bridge, vlan=vlan_tag) + + # add unreachable route to vrf + linux_net.add_unreachable_route(vrf) + + def _disconnect_evpn_to_ovn(self, vni, datapath_bridge, ips, + vlan_tag=None, cleanup_ndp_proxy=True): + vrf = constants.OVN_EVPN_VRF_PREFIX + str(vni) + # remove vrf from ovs bridge + ovs.del_device_from_ovs_bridge(vrf, datapath_bridge) + + linux_net.delete_routes_from_table(vni) + + if vlan_tag: + linux_net.delete_vlan_device_for_network(datapath_bridge, + vlan_tag) + elif cleanup_ndp_proxy: + for ip in ips: + if linux_net.get_ip_version(ip) == constants.IP_VERSION_6: + linux_net.del_ndp_proxy(ip, datapath_bridge) + + def _remove_extra_vrfs(self): + vrfs, los, bridges, vxlans = ([], [], [], []) + for cr_lrp_info in self.ovn_local_cr_lrps.values(): + vrfs.append(cr_lrp_info['vrf']) + los.append(cr_lrp_info['lo']) + bridges.append(cr_lrp_info['bridge']) + vxlans.append(cr_lrp_info['vxlan']) + + filter_out = ["{}.{}".format(key, value[0]['vlan']) + for key, value in self._ovn_routing_tables_routes.items() + if value[0]['vlan']] + + interfaces = linux_net.get_interfaces(filter_out) + for interface in interfaces: + if (interface.startswith(constants.OVN_EVPN_VRF_PREFIX) and + interface not in vrfs): + linux_net.delete_device(interface) + ovs.del_device_from_ovs_bridge(interface) + elif (interface.startswith(constants.OVN_EVPN_LO_PREFIX) and + interface not in los): + linux_net.delete_device(interface) + elif (interface.startswith(constants.OVN_EVPN_BRIDGE_PREFIX) and + (interface not in bridges and + interface != constants.OVN_INTEGRATION_BRIDGE and + interface not in set(self.ovn_bridge_mappings.values()))): + linux_net.delete_device(interface) + elif (interface.startswith(constants.OVN_EVPN_VXLAN_PREFIX) and + interface not in vxlans): + linux_net.delete_device(interface) + + def _remove_extra_routes(self): + table_ids = self._get_table_ids() + vrf_routes = linux_net.get_routes_on_tables(table_ids) + if not vrf_routes: + return + # remove from vrf_routes the routes that should be kept + for bridge, routes_info in self._ovn_routing_tables_routes.items(): + for route_info in routes_info: + oif = linux_net.get_interface_index(bridge) + if route_info['vlan']: + vlan_device_name = '{}.{}'.format(bridge, + route_info['vlan']) + oif = linux_net.get_interface_index(vlan_device_name) + if 'gateway' in route_info['route'].keys(): # subnet route + possible_matchings = [ + r for r in vrf_routes + if (r['dst'] == route_info['route']['dst'] and + r['dst_len'] == route_info['route']['dst_len'] and + r['gateway'] == route_info['route']['gateway'] and + r['table'] == route_info['route']['table'])] + else: # cr-lrp + possible_matchings = [ + r for r in vrf_routes + if (r['dst'] == route_info['route']['dst'] and + r['dst_len'] == route_info['route']['dst_len'] and + r['oif'] == oif and + r['table'] == route_info['route']['table'])] + for r in possible_matchings: + vrf_routes.remove(r) + + linux_net.delete_ip_routes(vrf_routes) + + def _remove_extra_ovs_flows(self): + cr_lrp_mac_vrf_mappings = self._get_cr_lrp_mac_vrf_mapping() + for bridge in set(self.ovn_bridge_mappings.values()): + current_flows = ovs.get_bridge_flows_by_cookie( + bridge, constants.OVS_VRF_RULE_COOKIE) + for flow in current_flows: + flow_info = ovs.get_flow_info(flow) + if not flow_info.get('mac'): + ovs.del_flow(flow, bridge, constants.OVS_VRF_RULE_COOKIE) + elif flow_info['mac'] not in cr_lrp_mac_vrf_mappings.keys(): + ovs.del_flow(flow, bridge, constants.OVS_VRF_RULE_COOKIE) + elif flow_info['port']: + if (not flow_info.get('nw_src') and not + flow_info.get('ipv6_src')): + ovs.del_flow(flow, bridge, + constants.OVS_VRF_RULE_COOKIE) + else: + device = cr_lrp_mac_vrf_mappings[flow_info['mac']] + vrf_port = ovs.get_device_port_at_ovs(device) + if vrf_port != flow_info['port']: + ovs.del_flow(flow, bridge, + constants.OVS_VRF_RULE_COOKIE) + nw_src_ip = nw_src_mask = None + matching_dst = False + if flow_info.get('nw_src'): + nw_src_ip = flow_info['nw_src'].split('/')[0] + nw_src_mask = int( + flow_info['nw_src'].split('/')[1]) + elif flow_info.get('ipv6_src'): + nw_src_ip = flow_info['ipv6_src'].split('/')[0] + nw_src_mask = int( + flow_info['ipv6_src'].split('/')[1]) + + for route_info in self._ovn_routing_tables_routes[ + bridge]: + if (route_info['route']['dst'] == nw_src_ip and + route_info['route'][ + 'dst_len'] == nw_src_mask): + matching_dst = True + if not matching_dst: + ovs.del_flow(flow, bridge, + constants.OVS_VRF_RULE_COOKIE) + + def _remove_extra_exposed_ips(self): + for lo, ips in self._ovn_exposed_evpn_ips.items(): + exposed_ips_on_device = linux_net.get_exposed_ips(lo) + for ip in exposed_ips_on_device: + if ip not in ips: + linux_net.del_ips_from_dev(lo, [ip]) + + def _get_table_ids(self): + table_ids = [] + for cr_lrp_info in self.ovn_local_cr_lrps.values(): + table_ids.append(cr_lrp_info['vni']) + return table_ids + + def _get_cr_lrp_mac_vrf_mapping(self): + mac_vrf_mappings = {} + for cr_lrp_info in self.ovn_local_cr_lrps.values(): + mac_vrf_mappings[cr_lrp_info['mac']] = cr_lrp_info['vrf'] + return mac_vrf_mappings diff --git a/ovn_bgp_agent/drivers/openstack/utils/ovn.py b/ovn_bgp_agent/drivers/openstack/utils/ovn.py index eb44011e..d30cc331 100644 --- a/ovn_bgp_agent/drivers/openstack/utils/ovn.py +++ b/ovn_bgp_agent/drivers/openstack/utils/ovn.py @@ -133,11 +133,13 @@ class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend): def is_provider_network(self, datapath): cmd = self.db_find_rows('Port_Binding', ('datapath', '=', datapath), - ('type', '=', 'localnet')) + ('type', '=', + constants.OVN_LOCALNET_VIF_PORT_TYPE)) return next(iter(cmd.execute(check_error=True)), None) def get_fip_associated(self, port): - cmd = self.db_find_rows('Port_Binding', ('type', '=', 'patch')) + cmd = self.db_find_rows( + 'Port_Binding', ('type', '=', constants.OVN_PATCH_VIF_PORT_TYPE)) for row in cmd.execute(check_error=True): for fip in row.nat_addresses: if port in fip: diff --git a/ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py b/ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py index 2edeb773..16c986d5 100644 --- a/ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py +++ b/ovn_bgp_agent/drivers/openstack/watchers/bgp_watcher.py @@ -12,12 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ovn_bgp_agent import constants - +from oslo_concurrency import lockutils +from oslo_log import log as logging from ovsdbapp.backend.ovs_idl import event as row_event -from oslo_concurrency import lockutils +from ovn_bgp_agent import constants + +LOG = logging.getLogger(__name__) _SYNC_STATE_LOCK = lockutils.ReaderWriterLock() @@ -29,6 +31,9 @@ class PortBindingChassisEvent(row_event.RowEvent): events, table, None) self.event_name = self.__class__.__name__ + def _check_single_dual_stack_format(mac): + return len(mac.split(' ')) in [2, 3] + class PortBindingChassisCreatedEvent(PortBindingChassisEvent): def __init__(self, bgp_agent): @@ -39,8 +44,7 @@ class PortBindingChassisCreatedEvent(PortBindingChassisEvent): def match_fn(self, event, row, old): try: # single and dual-stack format - if (len(row.mac[0].split(' ')) != 2 and - len(row.mac[0].split(' ')) != 3): + if not self._check_single_dual_stack_format(row.mac[0]): return False return (row.chassis[0].name == self.agent.chassis and not old.chassis) @@ -67,8 +71,7 @@ class PortBindingChassisDeletedEvent(PortBindingChassisEvent): def match_fn(self, event, row, old): try: # single and dual-stack format - if (len(row.mac[0].split(' ')) != 2 and - len(row.mac[0].split(' ')) != 3): + if not self._check_single_dual_stack_format(row.mac[0]): return False if event == self.ROW_UPDATE: return (old.chassis[0].name == self.agent.chassis and @@ -105,7 +108,7 @@ class FIPSetEvent(PortBindingChassisEvent): return False def run(self, event, row, old): - if row.type != 'patch': + if row.type != constants.OVN_PATCH_VIF_PORT_TYPE: return with _SYNC_STATE_LOCK.read_lock(): for nat in row.nat_addresses: @@ -130,7 +133,7 @@ class FIPUnsetEvent(PortBindingChassisEvent): return False def run(self, event, row, old): - if row.type != 'patch': + if row.type != constants.OVN_PATCH_VIF_PORT_TYPE: return with _SYNC_STATE_LOCK.read_lock(): for nat in old.nat_addresses: @@ -149,15 +152,14 @@ class SubnetRouterAttachedEvent(PortBindingChassisEvent): def match_fn(self, event, row, old): try: # single and dual-stack format - if (len(row.mac[0].split(' ')) != 2 and - len(row.mac[0].split(' ')) != 3): + if not self._check_single_dual_stack_format(row.mac[0]): return False return (not row.chassis and row.logical_port.startswith('lrp-')) except (IndexError, AttributeError): return False def run(self, event, row, old): - if row.type != 'patch': + if row.type != constants.OVN_PATCH_VIF_PORT_TYPE: return with _SYNC_STATE_LOCK.read_lock(): ip_address = row.mac[0].split(' ')[1] @@ -173,15 +175,14 @@ class SubnetRouterDetachedEvent(PortBindingChassisEvent): def match_fn(self, event, row, old): try: # single and dual-stack format - if (len(row.mac[0].split(' ')) != 2 and - len(row.mac[0].split(' ')) != 3): + if not self._check_single_dual_stack_format(row.mac[0]): return False return (not row.chassis and row.logical_port.startswith('lrp-')) except (IndexError, AttributeError): return False def run(self, event, row, old): - if row.type != 'patch': + if row.type != constants.OVN_PATCH_VIF_PORT_TYPE: return with _SYNC_STATE_LOCK.read_lock(): ip_address = row.mac[0].split(' ')[1] @@ -197,8 +198,7 @@ class TenantPortCreatedEvent(PortBindingChassisEvent): def match_fn(self, event, row, old): try: # single and dual-stack format - if (len(row.mac[0].split(' ')) != 2 and - len(row.mac[0].split(' ')) != 3): + if not self._check_single_dual_stack_format(row.mac[0]): return False return (not old.chassis and self.agent.ovn_local_lrps != []) @@ -226,8 +226,7 @@ class TenantPortDeletedEvent(PortBindingChassisEvent): def match_fn(self, event, row, old): try: # single and dual-stack format - if (len(row.mac[0].split(' ')) != 2 and - len(row.mac[0].split(' ')) != 3): + if not self._check_single_dual_stack_format(row.mac[0]): return False return (self.agent.ovn_local_lrps != []) except (IndexError, AttributeError): @@ -260,7 +259,7 @@ class ChassisCreateEventBase(row_event.RowEvent): if self.first_time: self.first_time = False else: - print("Connection to OVSDB established, doing a full sync") + LOG.info("Connection to OVSDB established, doing a full sync") self.agent.sync() diff --git a/ovn_bgp_agent/drivers/openstack/watchers/evpn_watcher.py b/ovn_bgp_agent/drivers/openstack/watchers/evpn_watcher.py new file mode 100644 index 00000000..fd620477 --- /dev/null +++ b/ovn_bgp_agent/drivers/openstack/watchers/evpn_watcher.py @@ -0,0 +1,246 @@ +# Copyright 2021 Red Hat, Inc. +# +# 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. + +from oslo_concurrency import lockutils +from oslo_log import log as logging +from ovsdbapp.backend.ovs_idl import event as row_event + +from ovn_bgp_agent import constants + + +LOG = logging.getLogger(__name__) + +_SYNC_STATE_LOCK = lockutils.ReaderWriterLock() + + +class PortBindingChassisEvent(row_event.RowEvent): + def __init__(self, bgp_agent, events): + self.agent = bgp_agent + table = 'Port_Binding' + super(PortBindingChassisEvent, self).__init__( + events, table, None) + self.event_name = self.__class__.__name__ + + def _check_single_dual_stack_format(mac): + return len(mac.split(' ')) in [2, 3] + + +class PortBindingChassisCreatedEvent(PortBindingChassisEvent): + def __init__(self, bgp_agent): + events = (self.ROW_UPDATE,) + super(PortBindingChassisCreatedEvent, self).__init__( + bgp_agent, events) + + def match_fn(self, event, row, old): + try: + if row.type != constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE: + return False + # single and dual-stack format + if not self._check_single_dual_stack_format(row.mac[0]): + return False + return (row.chassis[0].name == self.agent.chassis and + not old.chassis) + except (IndexError, AttributeError): + return False + + def run(self, event, row, old): + if row.type != constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE: + return + with _SYNC_STATE_LOCK.read_lock(): + self.agent.expose_ip(row, cr_lrp=True) + + +class PortBindingChassisDeletedEvent(PortBindingChassisEvent): + def __init__(self, bgp_agent): + events = (self.ROW_UPDATE, self.ROW_DELETE,) + super(PortBindingChassisDeletedEvent, self).__init__( + bgp_agent, events) + + def match_fn(self, event, row, old): + try: + if row.type != constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE: + return False + # single and dual-stack format + if not self._check_single_dual_stack_format(row.mac[0]): + return False + if event == self.ROW_UPDATE: + return (old.chassis[0].name == self.agent.chassis and + not row.chassis) + else: + if row.chassis[0].name == self.agent.chassis: + return True + except (IndexError, AttributeError): + return False + + def run(self, event, row, old): + if row.type != constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE: + return + with _SYNC_STATE_LOCK.read_lock(): + self.agent.withdraw_ip(row, cr_lrp=True) + + +class SubnetRouterAttachedEvent(PortBindingChassisEvent): + def __init__(self, bgp_agent): + events = (self.ROW_UPDATE, self.ROW_CREATE,) + super(SubnetRouterAttachedEvent, self).__init__( + bgp_agent, events) + + def match_fn(self, event, row, old): + try: + if event == self.ROW_UPDATE: + return (not row.chassis and + not row.logical_port.startswith('lrp-') and + row.external_ids[constants.OVN_EVPN_VNI_EXT_ID_KEY] and + row.external_ids[constants.OVN_EVPN_AS_EXT_ID_KEY] and + (not old.external_ids.get( + constants.OVN_EVPN_VNI_EXT_ID_KEY) or + not old.external_ids.get( + constants.constants.OVN_EVPN_AS_EXT_ID_KEY))) + else: + return (not row.chassis and + not row.logical_port.startswith('lrp-') and + row.external_ids[constants.OVN_EVPN_VNI_EXT_ID_KEY] and + row.external_ids[constants.OVN_EVPN_AS_EXT_ID_KEY]) + except (IndexError, AttributeError, KeyError): + return False + + def run(self, event, row, old): + if row.type != constants.OVN_PATCH_VIF_PORT_TYPE: + return + with _SYNC_STATE_LOCK.read_lock(): + if row.nat_addresses: + self.agent.expose_ip(row) + else: + self.agent.expose_subnet(row) + + +class SubnetRouterDetachedEvent(PortBindingChassisEvent): + def __init__(self, bgp_agent): + events = (self.ROW_UPDATE, self.ROW_DELETE,) + super(SubnetRouterDetachedEvent, self).__init__( + bgp_agent, events) + + def match_fn(self, event, row, old): + try: + if event == self.ROW_UPDATE: + return (not row.chassis and + not row.logical_port.startswith('lrp-') and + old.external_ids[constants.OVN_EVPN_VNI_EXT_ID_KEY] and + old.external_ids[constants.OVN_EVPN_AS_EXT_ID_KEY] and + (not row.external_ids.get( + constants.OVN_EVPN_VNI_EXT_ID_KEY) or + not row.external_ids.get( + constants.OVN_EVPN_AS_EXT_ID_KEY))) + else: + return (not row.chassis and + not row.logical_port.startswith('lrp-') and + row.external_ids[constants.OVN_EVPN_VNI_EXT_ID_KEY] and + row.external_ids[constants.OVN_EVPN_AS_EXT_ID_KEY]) + except (IndexError, AttributeError, KeyError): + return False + + def run(self, event, row, old): + if row.type != constants.OVN_PATCH_VIF_PORT_TYPE: + return + with _SYNC_STATE_LOCK.read_lock(): + if row.nat_addresses: + self.agent.withdraw_ip(row) + else: + self.agent.withdraw_subnet(row) + + +class TenantPortCreatedEvent(PortBindingChassisEvent): + def __init__(self, bgp_agent): + events = (self.ROW_UPDATE,) + super(TenantPortCreatedEvent, self).__init__( + bgp_agent, events) + + def match_fn(self, event, row, old): + try: + # single and dual-stack format + if not self._check_single_dual_stack_format(row.mac[0]): + return False + return (not old.chassis and row.chassis and + self.agent.ovn_local_lrps != []) + except (IndexError, AttributeError): + return False + + def run(self, event, row, old): + if row.type not in (constants.OVN_VM_VIF_PORT_TYPE, + constants.OVN_VIRTUAL_VIF_PORT_TYPE): + return + with _SYNC_STATE_LOCK.read_lock(): + ips = [row.mac[0].split(' ')[1]] + # for dual-stack + if len(row.mac[0].split(' ')) == 3: + ips.append(row.mac[0].split(' ')[2]) + self.agent.expose_remote_ip(ips, row) + + +class TenantPortDeletedEvent(PortBindingChassisEvent): + def __init__(self, bgp_agent): + events = (self.ROW_DELETE, self.ROW_UPDATE,) + super(TenantPortDeletedEvent, self).__init__( + bgp_agent, events) + + def match_fn(self, event, row, old): + try: + # single and dual-stack format + if not self._check_single_dual_stack_format(row.mac[0]): + return False + if event == self.ROW_UPDATE: + return (old.chassis and not row.chassis and + self.agent.ovn_local_lrps != []) + else: + return (self.agent.ovn_local_lrps != []) + except (IndexError, AttributeError): + return False + + def run(self, event, row, old): + if row.type not in (constants.OVN_VM_VIF_PORT_TYPE, + constants.OVN_VIRTUAL_VIF_PORT_TYPE): + return + with _SYNC_STATE_LOCK.read_lock(): + ips = [row.mac[0].split(' ')[1]] + # for dual-stack + if len(row.mac[0].split(' ')) == 3: + ips.append(row.mac[0].split(' ')[2]) + self.agent.withdraw_remote_ip(ips, row) + + +class ChassisCreateEventBase(row_event.RowEvent): + table = None + + def __init__(self, bgp_agent): + self.agent = bgp_agent + self.first_time = True + events = (self.ROW_CREATE,) + super(ChassisCreateEventBase, self).__init__( + events, self.table, (('name', '=', self.agent.chassis),)) + self.event_name = self.__class__.__name__ + + def run(self, event, row, old): + if self.first_time: + self.first_time = False + else: + LOG.info("Connection to OVSDB established, doing a full sync") + self.agent.sync() + + +class ChassisCreateEvent(ChassisCreateEventBase): + table = 'Chassis' + + +class ChassisPrivateCreateEvent(ChassisCreateEventBase): + table = 'Chassis_Private' diff --git a/setup.cfg b/setup.cfg index c21a2cef..1e830051 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,4 +29,5 @@ console_scripts = bgp-agent = ovn_bgp_agent.cmd.agent:start ovn_bgp_agent.drivers = - osp_ovn_bgp_driver = ovn_bgp_agent.drivers.openstack.ovn_bgp_driver:OSPOVNBGPDriver + ovn_bgp_driver = ovn_bgp_agent.drivers.openstack.ovn_bgp_driver:OVNBGPDriver + ovn_evpn_driver = ovn_bgp_agent.drivers.openstack.ovn_evpn_driver:OVNEVPNDriver