Add initial support for EVPN

Change-Id: I8c6ffc192158b96ea3186501ae6579bd2934d37b
This commit is contained in:
Luis Tomas Bolivar 2021-08-30 13:27:32 +02:00
parent d1d9b63e0f
commit e3afbb0aba
9 changed files with 1043 additions and 30 deletions

View File

@ -14,4 +14,4 @@ Features
* Expose VMs with FIPs or on Provider Networks through BGP on OVN * Expose VMs with FIPs or on Provider Networks through BGP on OVN
environments. environments.
* Expose VMs on Tenant Networks through EVPN on OVN environments.

View File

@ -40,10 +40,10 @@ the OVN overlay.
This simple design allows the agent to implement different drivers, depending This simple design allows the agent to implement different drivers, depending
on what OVN SB DB events are being watched (watchers examples at 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 triggered in reaction to them (drivers examples at
``networking_bgp_ovn/drivers/openstack/XXXX_driver.py``, implementing the ``ovn_bgp_agent/drivers/openstack/XXXX_driver.py``, implementing the
``networking_bgp_von/drivers/driver_api.py``). ``ovn_bgp_agent/drivers/driver_api.py``).
A new driver implements the support for EVPN capabilities with multitenancy A new driver implements the support for EVPN capabilities with multitenancy
(overlapping CIDRs), by leveraging VRFs and EVPN Type-5 Routes. The API used (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 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 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 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 from/to the OVN overlay. The only configuration needed is to enable the

View File

@ -33,7 +33,14 @@ IP_VERSION_6 = 6
IP_VERSION_4 = 4 IP_VERSION_4 = 4
BGP_MODE = 'BGP' 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' OVN_INTEGRATION_BRIDGE = 'br-int'
LINK_UP = "up" LINK_UP = "up"

View File

@ -37,7 +37,7 @@ LOG = logging.getLogger(__name__)
OVN_TABLES = ("Port_Binding", "Chassis", "Datapath_Binding", "Chassis_Private") OVN_TABLES = ("Port_Binding", "Chassis", "Datapath_Binding", "Chassis_Private")
class OSPOVNBGPDriver(driver_api.AgentDriverBase): class OVNBGPDriver(driver_api.AgentDriverBase):
def __init__(self): def __init__(self):
self._expose_tenant_networks = CONF.expose_tenant_networks 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.ovs_idl.start(constants.OVS_CONNECTION_STRING)
self.chassis = self.ovs_idl.get_own_chassis_name() self.chassis = self.ovs_idl.get_own_chassis_name()
self.ovn_remote = self.ovs_idl.get_ovn_remote() self.ovn_remote = self.ovs_idl.get_ovn_remote()
LOG.debug("Loaded chassis {}.".format(self.chassis)) LOG.debug("Loaded chassis %s.", self.chassis)
events = () events = ()
for event in self._get_events(): for event in self._get_events():

View File

@ -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

View File

@ -133,11 +133,13 @@ class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend):
def is_provider_network(self, datapath): def is_provider_network(self, datapath):
cmd = self.db_find_rows('Port_Binding', ('datapath', '=', 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) return next(iter(cmd.execute(check_error=True)), None)
def get_fip_associated(self, port): 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 row in cmd.execute(check_error=True):
for fip in row.nat_addresses: for fip in row.nat_addresses:
if port in fip: if port in fip:

View File

@ -12,12 +12,14 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # 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 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() _SYNC_STATE_LOCK = lockutils.ReaderWriterLock()
@ -29,6 +31,9 @@ class PortBindingChassisEvent(row_event.RowEvent):
events, table, None) events, table, None)
self.event_name = self.__class__.__name__ self.event_name = self.__class__.__name__
def _check_single_dual_stack_format(mac):
return len(mac.split(' ')) in [2, 3]
class PortBindingChassisCreatedEvent(PortBindingChassisEvent): class PortBindingChassisCreatedEvent(PortBindingChassisEvent):
def __init__(self, bgp_agent): def __init__(self, bgp_agent):
@ -39,8 +44,7 @@ class PortBindingChassisCreatedEvent(PortBindingChassisEvent):
def match_fn(self, event, row, old): def match_fn(self, event, row, old):
try: try:
# single and dual-stack format # single and dual-stack format
if (len(row.mac[0].split(' ')) != 2 and if not self._check_single_dual_stack_format(row.mac[0]):
len(row.mac[0].split(' ')) != 3):
return False return False
return (row.chassis[0].name == self.agent.chassis and return (row.chassis[0].name == self.agent.chassis and
not old.chassis) not old.chassis)
@ -67,8 +71,7 @@ class PortBindingChassisDeletedEvent(PortBindingChassisEvent):
def match_fn(self, event, row, old): def match_fn(self, event, row, old):
try: try:
# single and dual-stack format # single and dual-stack format
if (len(row.mac[0].split(' ')) != 2 and if not self._check_single_dual_stack_format(row.mac[0]):
len(row.mac[0].split(' ')) != 3):
return False return False
if event == self.ROW_UPDATE: if event == self.ROW_UPDATE:
return (old.chassis[0].name == self.agent.chassis and return (old.chassis[0].name == self.agent.chassis and
@ -105,7 +108,7 @@ class FIPSetEvent(PortBindingChassisEvent):
return False return False
def run(self, event, row, old): def run(self, event, row, old):
if row.type != 'patch': if row.type != constants.OVN_PATCH_VIF_PORT_TYPE:
return return
with _SYNC_STATE_LOCK.read_lock(): with _SYNC_STATE_LOCK.read_lock():
for nat in row.nat_addresses: for nat in row.nat_addresses:
@ -130,7 +133,7 @@ class FIPUnsetEvent(PortBindingChassisEvent):
return False return False
def run(self, event, row, old): def run(self, event, row, old):
if row.type != 'patch': if row.type != constants.OVN_PATCH_VIF_PORT_TYPE:
return return
with _SYNC_STATE_LOCK.read_lock(): with _SYNC_STATE_LOCK.read_lock():
for nat in old.nat_addresses: for nat in old.nat_addresses:
@ -149,15 +152,14 @@ class SubnetRouterAttachedEvent(PortBindingChassisEvent):
def match_fn(self, event, row, old): def match_fn(self, event, row, old):
try: try:
# single and dual-stack format # single and dual-stack format
if (len(row.mac[0].split(' ')) != 2 and if not self._check_single_dual_stack_format(row.mac[0]):
len(row.mac[0].split(' ')) != 3):
return False return False
return (not row.chassis and row.logical_port.startswith('lrp-')) return (not row.chassis and row.logical_port.startswith('lrp-'))
except (IndexError, AttributeError): except (IndexError, AttributeError):
return False return False
def run(self, event, row, old): def run(self, event, row, old):
if row.type != 'patch': if row.type != constants.OVN_PATCH_VIF_PORT_TYPE:
return return
with _SYNC_STATE_LOCK.read_lock(): with _SYNC_STATE_LOCK.read_lock():
ip_address = row.mac[0].split(' ')[1] ip_address = row.mac[0].split(' ')[1]
@ -173,15 +175,14 @@ class SubnetRouterDetachedEvent(PortBindingChassisEvent):
def match_fn(self, event, row, old): def match_fn(self, event, row, old):
try: try:
# single and dual-stack format # single and dual-stack format
if (len(row.mac[0].split(' ')) != 2 and if not self._check_single_dual_stack_format(row.mac[0]):
len(row.mac[0].split(' ')) != 3):
return False return False
return (not row.chassis and row.logical_port.startswith('lrp-')) return (not row.chassis and row.logical_port.startswith('lrp-'))
except (IndexError, AttributeError): except (IndexError, AttributeError):
return False return False
def run(self, event, row, old): def run(self, event, row, old):
if row.type != 'patch': if row.type != constants.OVN_PATCH_VIF_PORT_TYPE:
return return
with _SYNC_STATE_LOCK.read_lock(): with _SYNC_STATE_LOCK.read_lock():
ip_address = row.mac[0].split(' ')[1] ip_address = row.mac[0].split(' ')[1]
@ -197,8 +198,7 @@ class TenantPortCreatedEvent(PortBindingChassisEvent):
def match_fn(self, event, row, old): def match_fn(self, event, row, old):
try: try:
# single and dual-stack format # single and dual-stack format
if (len(row.mac[0].split(' ')) != 2 and if not self._check_single_dual_stack_format(row.mac[0]):
len(row.mac[0].split(' ')) != 3):
return False return False
return (not old.chassis and return (not old.chassis and
self.agent.ovn_local_lrps != []) self.agent.ovn_local_lrps != [])
@ -226,8 +226,7 @@ class TenantPortDeletedEvent(PortBindingChassisEvent):
def match_fn(self, event, row, old): def match_fn(self, event, row, old):
try: try:
# single and dual-stack format # single and dual-stack format
if (len(row.mac[0].split(' ')) != 2 and if not self._check_single_dual_stack_format(row.mac[0]):
len(row.mac[0].split(' ')) != 3):
return False return False
return (self.agent.ovn_local_lrps != []) return (self.agent.ovn_local_lrps != [])
except (IndexError, AttributeError): except (IndexError, AttributeError):
@ -260,7 +259,7 @@ class ChassisCreateEventBase(row_event.RowEvent):
if self.first_time: if self.first_time:
self.first_time = False self.first_time = False
else: else:
print("Connection to OVSDB established, doing a full sync") LOG.info("Connection to OVSDB established, doing a full sync")
self.agent.sync() self.agent.sync()

View File

@ -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'

View File

@ -29,4 +29,5 @@ console_scripts =
bgp-agent = ovn_bgp_agent.cmd.agent:start bgp-agent = ovn_bgp_agent.cmd.agent:start
ovn_bgp_agent.drivers = 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