Add a new driver that uses NB DB instead of SB DB

This patch creates a new driver using NB DB information instead of
SB DB. This has 2 objective:
- Alleviate the stress on big scale environment due to many
  connections to the SB DBs.
- Being more future proof as content generated in the SB DBs based
  on NB DB information is subject to change. For example we were
  already adviced that the information we are currently using for
  ovn LB events at the SB DB is probably going to change soon.

Depends-On: https://review.opendev.org/c/openstack/ovsdbapp/+/873853
Change-Id: Ib6bf077ce1e354652f5b728bd7192c762d3d071b
This commit is contained in:
Luis Tomas Bolivar 2022-11-10 07:55:00 +01:00
parent ddb740e2e4
commit cb978dc244
16 changed files with 1902 additions and 11 deletions

View File

@ -45,18 +45,33 @@ agent_opts = [
help='The connection string for the native OVSDB backend.\n' help='The connection string for the native OVSDB backend.\n'
'Use tcp:IP:PORT for TCP connection.\n' 'Use tcp:IP:PORT for TCP connection.\n'
'Use unix:FILE for unix domain socket connection.'), 'Use unix:FILE for unix domain socket connection.'),
cfg.IntOpt('ovsdb_connection_timeout',
default=180,
help='Timeout in seconds for the OVSDB connection transaction'),
cfg.StrOpt('ovn_sb_private_key', cfg.StrOpt('ovn_sb_private_key',
default='/etc/pki/tls/private/ovn_controller.key', default='/etc/pki/tls/private/ovn_bgp_agent.key',
help='The PEM file with private key for SSL connection to ' help='The PEM file with private key for SSL connection to '
'OVN-SB-DB'), 'OVN-SB-DB'),
cfg.StrOpt('ovn_sb_certificate', cfg.StrOpt('ovn_sb_certificate',
default='/etc/pki/tls/certs/ovn_controller.crt', default='/etc/pki/tls/certs/ovn_bgp_agent.crt',
help='The PEM file with certificate that certifies the ' help='The PEM file with certificate that certifies the '
'private key specified in ovn_sb_private_key'), 'private key specified in ovn_sb_private_key'),
cfg.StrOpt('ovn_sb_ca_cert', cfg.StrOpt('ovn_sb_ca_cert',
default='/etc/ipa/ca.crt', default='/etc/ipa/ca.crt',
help='The PEM file with CA certificate that OVN should use to' help='The PEM file with CA certificate that OVN should use to'
' verify certificates presented to it by SSL peers'), ' verify certificates presented to it by SSL peers'),
cfg.StrOpt('ovn_nb_private_key',
default='/etc/pki/tls/private/ovn_bgp_agent.key',
help='The PEM file with private key for SSL connection to '
'OVN-NB-DB'),
cfg.StrOpt('ovn_nb_certificate',
default='/etc/pki/tls/certs/ovn_bgp_agent.crt',
help='The PEM file with certificate that certifies the '
'private key specified in ovn_nb_private_key'),
cfg.StrOpt('ovn_nb_ca_cert',
default='/etc/ipa/ca.crt',
help='The PEM file with CA certificate that OVN should use to'
' verify certificates presented to it by SSL peers'),
cfg.StrOpt('bgp_AS', cfg.StrOpt('bgp_AS',
default='64999', default='64999',
help='AS number to be used by the Agent when running in BGP ' help='AS number to be used by the Agent when running in BGP '

View File

@ -19,9 +19,13 @@ OVN_VM_VIF_PORT_TYPE = ""
OVN_PATCH_VIF_PORT_TYPE = "patch" OVN_PATCH_VIF_PORT_TYPE = "patch"
OVN_CHASSISREDIRECT_VIF_PORT_TYPE = "chassisredirect" OVN_CHASSISREDIRECT_VIF_PORT_TYPE = "chassisredirect"
OVN_LOCALNET_VIF_PORT_TYPE = "localnet" OVN_LOCALNET_VIF_PORT_TYPE = "localnet"
OVN_DNAT_AND_SNAT = "dnat_and_snat"
OVN_CIDRS_EXT_ID_KEY = 'neutron:cidrs' OVN_CIDRS_EXT_ID_KEY = 'neutron:cidrs'
OVN_PORT_NAME_EXT_ID_KEY = 'neutron:port_name' OVN_PORT_NAME_EXT_ID_KEY = 'neutron:port_name'
OVN_LS_NAME_EXT_ID_KEY = 'neutron:network_name'
OVN_FIP_EXT_ID_KEY = 'neutron:port_fip'
OVN_FIP_NET_EXT_ID_KEY = 'neutron:fip_network_id'
LB_VIP_PORT_PREFIX = "ovn-lb-vip-" LB_VIP_PORT_PREFIX = "ovn-lb-vip-"
OVS_RULE_COOKIE = "999" OVS_RULE_COOKIE = "999"
@ -58,3 +62,5 @@ SUBNET_POOL_ADDR_SCOPE6 = "neutron:subnet_pool_addr_scope6"
EXPOSE = "expose" EXPOSE = "expose"
WITHDRAW = "withdraw" WITHDRAW = "withdraw"
OVN_REQUESTED_CHASSIS = "requested-chassis"

View File

@ -0,0 +1,460 @@
# Copyright 2023 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 pyroute2
import threading
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 bgp as bgp_utils
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.utils import wire as wire_utils
from ovn_bgp_agent.drivers.openstack.watchers import nb_bgp_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 = ["Logical_Switch_Port", "NAT", "Logical_Switch"]
class NBOVNBGPDriver(driver_api.AgentDriverBase):
def __init__(self):
self._expose_tenant_networks = (CONF.expose_tenant_networks or
CONF.expose_ipv6_gua_tenant_networks)
self.allowed_address_scopes = set(CONF.address_scopes or [])
self.ovn_routing_tables = {} # {'br-ex': 200}
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()
# {ovn_lb: VIP1, VIP2}
self.ovn_lb_vips = collections.defaultdict()
self.ovn_fips = {} # {'fip': {'bridge_device': X, 'bridge_vlan': Y}}
# {'ls_name': {'bridge_device': X, 'bridge_vlan': Y}}
self.ovn_provider_ls = {}
# dict instead of list to speed up look ups
self.ovn_tenant_ls = {} # {'ls_name': True}
self._nb_idl = None
self._post_start_event = threading.Event()
@property
def nb_idl(self):
if not self._nb_idl:
self._post_start_event.wait()
return self._nb_idl
@nb_idl.setter
def nb_idl(self, val):
self._nb_idl = val
def start(self):
self.ovs_idl = ovs.OvsIdl()
self.ovs_idl.start(CONF.ovsdb_connection)
self.chassis = self.ovs_idl.get_own_chassis_name()
# NOTE(ltomasbo): remote should point to NB DB port instead of SB DB,
# so changing 6642 by 6641
self.ovn_remote = self.ovs_idl.get_ovn_remote().replace(":6642",
":6641")
LOG.info("Loaded chassis %s.", self.chassis)
LOG.info("Starting VRF configuration for advertising routes")
# Create VRF
linux_net.ensure_vrf(CONF.bgp_vrf, CONF.bgp_vrf_table_id)
# Ensure FRR is configure to leak the routes
# NOTE: If we want to recheck this every X time, we should move it
# inside the sync function instead
frr.vrf_leak(CONF.bgp_vrf, CONF.bgp_AS, CONF.bgp_router_id)
# Create OVN dummy device
linux_net.ensure_ovn_device(CONF.bgp_nic, CONF.bgp_vrf)
# Clear vrf routing table
if CONF.clear_vrf_routes_on_startup:
linux_net.delete_routes_from_table(CONF.bgp_vrf_table_id)
LOG.info("VRF configuration for advertising routes completed")
if self._expose_tenant_networks and self.allowed_address_scopes:
LOG.info("Configured allowed address scopes: %s",
", ".join(self.allowed_address_scopes))
events = ()
for event in self._get_events():
event_class = getattr(watcher, event)
events += (event_class(self),)
self._post_start_event.clear()
self.nb_idl = ovn.OvnNbIdl(
self.ovn_remote,
tables=OVN_TABLES,
events=events).start()
# Now IDL connections can be safely used
self._post_start_event.set()
def _get_events(self):
events = set(["LogicalSwitchPortProviderCreateEvent",
"LogicalSwitchPortProviderDeleteEvent",
"LogicalSwitchPortFIPCreateEvent",
"LogicalSwitchPortFIPDeleteEvent",
"LocalnetCreateDeleteEvent"])
if self._expose_tenant_networks:
events.update([])
return events
@lockutils.synchronized('nbbgp')
def sync(self):
self._expose_tenant_networks = (CONF.expose_tenant_networks or
CONF.expose_ipv6_gua_tenant_networks)
self.ovn_local_cr_lrps = {}
self.ovn_local_lrps = {}
self.ovn_routing_tables_routes = collections.defaultdict()
self.ovn_lb_vips = collections.defaultdict()
self.ovn_provider_ls = {}
self.ovn_tenant_ls = {}
LOG.debug("Ensuring VRF configuration for advertising routes")
# Create VRF
linux_net.ensure_vrf(CONF.bgp_vrf,
CONF.bgp_vrf_table_id)
# Create OVN dummy device
linux_net.ensure_ovn_device(CONF.bgp_nic,
CONF.bgp_vrf)
LOG.debug("Configuring br-ex default rule and routing tables for "
"each provider network")
flows_info = {}
# 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
extra_routes = {}
with pyroute2.NDB() as ndb:
for bridge_index, bridge_mapping in enumerate(bridge_mappings, 1):
network = bridge_mapping.split(":")[0]
bridge = bridge_mapping.split(":")[1]
self.ovn_bridge_mappings[network] = bridge
if not extra_routes.get(bridge):
extra_routes[bridge] = (
linux_net.ensure_routing_table_for_bridge(
self.ovn_routing_tables, bridge,
CONF.bgp_vrf_table_id))
vlan_tag = self.nb_idl.get_network_vlan_tag_by_network_name(
network)
if vlan_tag:
vlan_tag = vlan_tag[0]
linux_net.ensure_vlan_device_for_network(bridge,
vlan_tag)
linux_net.ensure_arp_ndp_enabled_for_bridge(bridge,
bridge_index,
vlan_tag)
if flows_info.get(bridge):
continue
flows_info[bridge] = {
'mac': ndb.interfaces[bridge]['address'],
'in_port': set([])}
# 3) Get in_port for bridge mappings (br-ex, br-ex2)
ovs.get_ovs_flows_info(bridge, flows_info,
constants.OVS_RULE_COOKIE)
# 4) Add/Remove flows for each bridge mappings
ovs.remove_extra_ovs_flows(flows_info, constants.OVS_RULE_COOKIE)
LOG.debug("Syncing current routes.")
exposed_ips = linux_net.get_exposed_ips(CONF.bgp_nic)
# get the rules pointing to ovn bridges
ovn_ip_rules = linux_net.get_ovn_ip_rules(
self.ovn_routing_tables.values())
# add missing routes/ips for IPs on provider network
ports = self.nb_idl.get_active_ports_on_chassis(self.chassis)
for port in ports:
if port.type not in [constants.OVN_VM_VIF_PORT_TYPE,
constants.OVN_VIRTUAL_VIF_PORT_TYPE]:
continue
self._ensure_port_exposed(port, exposed_ips, ovn_ip_rules)
# remove extra routes/ips
# remove all the leftovers on the list of current ips on dev OVN
linux_net.delete_exposed_ips(exposed_ips, CONF.bgp_nic)
# remove all the leftovers on the list of current ip rules for ovn
# bridges
linux_net.delete_ip_rules(ovn_ip_rules)
# remove all the extra rules not needed
linux_net.delete_bridge_ip_routes(self.ovn_routing_tables,
self.ovn_routing_tables_routes,
extra_routes)
def _ensure_port_exposed(self, port, exposed_ips, ovn_ip_rules):
port_fip = port.external_ids.get(constants.OVN_FIP_EXT_ID_KEY)
if port_fip:
external_ip, ls_name = self.get_port_external_ip_and_ls(port.name)
if not external_ip or not ls_name:
return
if self._expose_fip(external_ip, ls_name):
ip_version = linux_net.get_ip_version(external_ip)
if ip_version == constants.IP_VERSION_6:
ip_dst = "{}/128".format(external_ip)
else:
ip_dst = "{}/32".format(external_ip)
if external_ip in exposed_ips:
exposed_ips.remove(external_ip)
ovn_ip_rules.pop(ip_dst, None)
return
logical_switch = port.external_ids.get(
constants.OVN_LS_NAME_EXT_ID_KEY)
if not logical_switch:
return
if self.ovn_tenant_ls.get(logical_switch):
return
bridge_info = self.ovn_provider_ls.get(logical_switch)
if bridge_info:
# already known provider ls
bridge_device = bridge_info['bridge_device']
bridge_vlan = bridge_info['bridge_vlan']
else:
bridge_device, bridge_vlan = self._get_ls_localnet_info(
logical_switch)
if not bridge_device:
# This means it is not a provider network
self.ovn_tenant_ls[logical_switch] = True
return False
self.ovn_provider_ls[logical_switch] = {
'bridge_device': bridge_device,
'bridge_vlan': bridge_vlan}
ips = port.addresses[0].strip().split(' ')[1:]
ips_adv = self._expose_ip(ips, bridge_device, bridge_vlan, port.type,
port.external_ids.get(
constants.OVN_CIDRS_EXT_ID_KEY))
for ip in ips_adv:
ip_version = linux_net.get_ip_version(ip)
if ip_version == constants.IP_VERSION_6:
ip_dst = "{}/128".format(ip)
else:
ip_dst = "{}/32".format(ip)
if ip in exposed_ips:
exposed_ips.remove(ip)
ovn_ip_rules.pop(ip_dst, None)
def _expose_provider_port(self, port_ips, bridge_device, bridge_vlan,
proxy_cidrs=None):
# Connect to OVN
if wire_utils.wire_provider_port(
self.ovn_routing_tables_routes, port_ips, bridge_device,
bridge_vlan, self.ovn_routing_tables[bridge_device],
proxy_cidrs):
# Expose the IP now that it is connected
bgp_utils.announce_ips(port_ips)
def _withdraw_provider_port(self, port_ips, bridge_device, bridge_vlan,
proxy_cidrs=None):
# Withdraw IP before disconnecting it
bgp_utils.withdraw_ips(port_ips)
# Disconnect IP from OVN
wire_utils.unwire_provider_port(
self.ovn_routing_tables_routes, port_ips, bridge_device,
bridge_vlan, self.ovn_routing_tables[bridge_device], proxy_cidrs)
def _get_bridge_for_localnet_port(self, localnet):
bridge_device = None
bridge_vlan = None
network_name = localnet.options.get('network_name')
if network_name:
bridge_device = self.ovn_bridge_mappings[network_name]
if localnet.tag:
bridge_vlan = localnet.tag[0]
return bridge_device, bridge_vlan
@lockutils.synchronized('nbbgp')
def expose_ip(self, ips, row):
'''Advertice BGP route by adding IP to device.
This methods ensures BGP advertises the IP of the VM in the provider
network.
It relies on Zebra, which creates and advertises a route when an IP
is added to a local interface.
This method assumes a device named self.ovn_device exists (inside a
VRF), and adds the IP of:
- VM IP on the provider network
'''
logical_switch = row.external_ids.get(constants.OVN_LS_NAME_EXT_ID_KEY)
if not logical_switch:
return False
bridge_device, bridge_vlan = self._get_ls_localnet_info(logical_switch)
if not bridge_device:
# This means it is not a provider network
self.ovn_tenant_ls[logical_switch] = True
return False
self.ovn_provider_ls[logical_switch] = {
'bridge_device': bridge_device,
'bridge_vlan': bridge_vlan}
return self._expose_ip(ips, bridge_device, bridge_vlan,
port_type=row.type, cidr=row.external_ids.get(
constants.OVN_CIDRS_EXT_ID_KEY))
def _expose_ip(self, ips, bridge_device, bridge_vlan, port_type, cidr):
LOG.debug("Adding BGP route for logical port with ip %s", ips)
if cidr and port_type == constants.OVN_VIRTUAL_VIF_PORT_TYPE:
# NOTE: For Amphora Load Balancer with IPv6 VIP on the provider
# network, we need a NDP Proxy so that the traffic from the
# amphora can properly be redirected back
self._expose_provider_port(ips, bridge_device, bridge_vlan, [cidr])
else:
self._expose_provider_port(ips, bridge_device, bridge_vlan)
LOG.debug("Added BGP route for logical port with ip %s", ips)
return ips
@lockutils.synchronized('nbbgp')
def withdraw_ip(self, ips, row):
'''Withdraw BGP route by removing IP from device.
This methods ensures BGP withdraw an advertised IP of a VM, either
in the provider network.
It relies on Zebra, which withdraws the advertisement as soon as the
IP is deleted from the local interface.
This method assumes a device named self.ovn_decice exists (inside a
VRF), and removes the IP of:
- VM IP on the provider network
'''
logical_switch = row.external_ids.get(constants.OVN_LS_NAME_EXT_ID_KEY)
if not logical_switch:
return
bridge_device, bridge_vlan = self._get_ls_localnet_info(logical_switch)
if not bridge_device:
# This means it is not a provider network
return
proxy_cidr = None
if row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE:
n_cidr = row.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY)
if n_cidr and (linux_net.get_ip_version(n_cidr) ==
constants.IP_VERSION_6):
if not self.nb_idl.ls_has_virtual_ports(logical_switch):
proxy_cidr = n_cidr
LOG.debug("Deleting BGP route for logical port with ip %s", ips)
if proxy_cidr:
self._withdraw_provider_port(ips, bridge_device, bridge_vlan,
[proxy_cidr])
else:
self._withdraw_provider_port(ips, bridge_device, bridge_vlan)
LOG.debug("Deleted BGP route for logical port with ip %s", ips)
def _get_ls_localnet_info(self, logical_switch):
localnet_ports = self.nb_idl.ls_get_localnet_ports(
logical_switch, if_exists=True).execute(check_error=True)
if not localnet_ports:
# means it is not a provider network, so no need to expose the IP
return None, None
# NOTE: assuming only one localnet per LS exists
return self._get_bridge_for_localnet_port(localnet_ports[0])
def get_port_external_ip_and_ls(self, port):
nat_entry = self.nb_idl.get_nat_by_logical_port(port)
if not nat_entry:
return
net_id = nat_entry.external_ids.get(constants.OVN_FIP_NET_EXT_ID_KEY)
if not net_id:
return nat_entry.external_ip, None
else:
return nat_entry.external_ip, "neutron-{}".format(net_id)
@lockutils.synchronized('nbbgp')
def expose_fip(self, ip, logical_switch):
'''Advertice BGP route by adding IP to device.
This methods ensures BGP advertises the FIP associated to a VM in a
tenant networks.
It relies on Zebra, which creates and advertises a route when an IP
is added to a local interface.
This method assumes a device named self.ovn_device exists (inside a
VRF), and adds the IP of:
- VM FIP
'''
return self._expose_fip(ip, logical_switch)
def _expose_fip(self, ip, logical_switch):
bridge_device, bridge_vlan = self._get_ls_localnet_info(logical_switch)
if not bridge_device:
# This means it is not a provider network
return False
LOG.debug("Adding BGP route for FIP with ip %s", ip)
self._expose_provider_port([ip], bridge_device, bridge_vlan)
self.ovn_fips[ip] = {'bridge_device': bridge_device,
'bridge_vlan': bridge_vlan}
LOG.debug("Added BGP route for FIP with ip %s", ip)
return True
@lockutils.synchronized('nbbgp')
def withdraw_fip(self, ip):
'''Withdraw BGP route by removing IP from device.
This methods ensures BGP withdraw an advertised the FIP associated to
a VM in a tenant networks.
It relies on Zebra, which withdraws the advertisement as soon as the
IP is deleted from the local interface.
This method assumes a device named self.ovn_decice exists (inside a
VRF), and removes the IP of:
- VM FIP
'''
fip_info = self.ovn_fips.get(ip)
if not fip_info:
# No information to withdraw the FIP
return
bridge_device = fip_info['bridge_device']
bridge_vlan = fip_info['bridge_vlan']
LOG.debug("Deleting BGP route for FIP with ip %s", ip)
self._withdraw_provider_port([ip], bridge_device, bridge_vlan)
LOG.debug("Deleted BGP route for FIP with ip %s", ip)
@lockutils.synchronized('nbbgp')
def expose_remote_ip(self, ips, row):
pass
@lockutils.synchronized('nbbgp')
def withdraw_remote_ip(self, ips, row, chassis=None):
pass
@lockutils.synchronized('nbbgp')
def expose_subnet(self, ip, row):
pass
@lockutils.synchronized('nbbgp')
def withdraw_subnet(self, ip, row):
pass

View File

@ -73,7 +73,7 @@ class OVNBGPDriver(driver_api.AgentDriverBase):
def start(self): def start(self):
self.ovs_idl = ovs.OvsIdl() self.ovs_idl = ovs.OvsIdl()
self.ovs_idl.start(CONF.ovsdb_connection) self.ovs_idl.start(CONF.ovsdb_connection)
self.chassis = self.ovs_idl.get_own_chassis_name() self.chassis = self.ovs_idl.get_own_chassis_id()
self.ovn_remote = self.ovs_idl.get_ovn_remote() self.ovn_remote = self.ovs_idl.get_ovn_remote()
LOG.info("Loaded chassis %s.", self.chassis) LOG.info("Loaded chassis %s.", self.chassis)
@ -509,7 +509,7 @@ class OVNBGPDriver(driver_api.AgentDriverBase):
It relies on Zebra, which creates and advertises a route when an IP It relies on Zebra, which creates and advertises a route when an IP
is added to a local interface. is added to a local interface.
This method assumes a device named self.ovn_decice exists (inside a This method assumes a device named self.ovn_device exists (inside a
VRF), and adds the IP of either: VRF), and adds the IP of either:
- VM IP on the provider network, - VM IP on the provider network,
- VM FIP, or - VM FIP, or
@ -639,7 +639,7 @@ class OVNBGPDriver(driver_api.AgentDriverBase):
It relies on Zebra, which withdraws the advertisement as soon as the It relies on Zebra, which withdraws the advertisement as soon as the
IP is deleted from the local interface. IP is deleted from the local interface.
This method assumes a device named self.ovn_decice exists (inside a This method assumes a device named self.ovn_device exists (inside a
VRF), and removes the IP of either: VRF), and removes the IP of either:
- VM IP on the provider network, - VM IP on the provider network,
- VM FIP, or - VM FIP, or

View File

@ -67,7 +67,7 @@ class OVNEVPNDriver(driver_api.AgentDriverBase):
def start(self): def start(self):
self.ovs_idl = ovs.OvsIdl() self.ovs_idl = ovs.OvsIdl()
self.ovs_idl.start(CONF.ovsdb_connection) self.ovs_idl.start(CONF.ovsdb_connection)
self.chassis = self.ovs_idl.get_own_chassis_name() self.chassis = self.ovs_idl.get_own_chassis_id()
self.ovn_remote = self.ovs_idl.get_ovn_remote() self.ovn_remote = self.ovs_idl.get_ovn_remote()
LOG.debug("Loaded chassis %s.", self.chassis) LOG.debug("Loaded chassis %s.", self.chassis)

View File

@ -88,7 +88,7 @@ class OVNBGPStretchedL2Driver(driver_api.AgentDriverBase):
if CONF.clear_vrf_routes_on_startup: if CONF.clear_vrf_routes_on_startup:
linux_net.delete_routes_from_table(CONF.bgp_vrf_table_id) linux_net.delete_routes_from_table(CONF.bgp_vrf_table_id)
self.chassis = self.ovs_idl.get_own_chassis_name() self.chassis = self.ovs_idl.get_own_chassis_id()
self.ovn_remote = self.ovs_idl.get_ovn_remote() self.ovn_remote = self.ovs_idl.get_ovn_remote()
LOG.debug("Loaded chassis %s.", self.chassis) LOG.debug("Loaded chassis %s.", self.chassis)
if self.allowed_address_scopes: if self.allowed_address_scopes:

View File

@ -20,6 +20,7 @@ from ovsdbapp.backend import ovs_idl
from ovsdbapp.backend.ovs_idl import connection from ovsdbapp.backend.ovs_idl import connection
from ovsdbapp.backend.ovs_idl import idlutils from ovsdbapp.backend.ovs_idl import idlutils
from ovsdbapp import event from ovsdbapp import event
from ovsdbapp.schema.ovn_northbound import impl_idl as nb_impl_idl
from ovsdbapp.schema.ovn_southbound import impl_idl as sb_impl_idl from ovsdbapp.schema.ovn_southbound import impl_idl as sb_impl_idl
from ovn_bgp_agent import constants from ovn_bgp_agent import constants
@ -45,6 +46,47 @@ class OvnDbNotifyHandler(event.RowEventHandler):
self.driver = driver self.driver = driver
class OvnNbIdl(OvnIdl):
SCHEMA = 'OVN_Northbound'
def __init__(self, connection_string, events=None, tables=None):
if connection_string.startswith("ssl"):
self._check_and_set_ssl_files(self.SCHEMA)
helper = self._get_ovsdb_helper(connection_string)
self._events = events
if tables is None:
tables = ('Logical_Switch_Port', 'NAT', 'NB_Global')
for table in tables:
helper.register_table(table)
super(OvnNbIdl, self).__init__(
None, connection_string, helper, leader_only=False)
def _get_ovsdb_helper(self, connection_string):
return idlutils.get_schema_helper(connection_string, self.SCHEMA)
def _check_and_set_ssl_files(self, schema_name):
priv_key_file = CONF.ovn_nb_private_key
cert_file = CONF.ovn_nb_certificate
ca_cert_file = CONF.ovn_nb_ca_cert
if priv_key_file:
Stream.ssl_set_private_key_file(priv_key_file)
if cert_file:
Stream.ssl_set_certificate_file(cert_file)
if ca_cert_file:
Stream.ssl_set_ca_cert_file(ca_cert_file)
def start(self):
conn = connection.Connection(
self, timeout=CONF.ovsdb_connection_timeout)
ovsdbNbConn = OvsdbNbOvnIdl(conn)
if self._events:
self.notify_handler.watch_events(self._events)
return ovsdbNbConn
class OvnSbIdl(OvnIdl): class OvnSbIdl(OvnIdl):
SCHEMA = 'OVN_Southbound' SCHEMA = 'OVN_Southbound'
@ -85,7 +127,7 @@ class OvnSbIdl(OvnIdl):
def start(self): def start(self):
conn = connection.Connection( conn = connection.Connection(
self, timeout=180) self, timeout=CONF.ovsdb_connection_timeout)
ovsdbSbConn = OvsdbSbOvnIdl(conn) ovsdbSbConn = OvsdbSbOvnIdl(conn)
if self._events: if self._events:
self.notify_handler.watch_events(self._events) self.notify_handler.watch_events(self._events)
@ -109,6 +151,41 @@ class Backend(ovs_idl.Backend):
return self.idl.tables return self.idl.tables
class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend):
def __init__(self, connection):
super(OvsdbNbOvnIdl, self).__init__(connection)
self.idl._session.reconnect.set_probe_interval(60000)
def get_network_vlan_tag_by_network_name(self, network_name):
cmd = self.db_find_rows('Logical_Switch_Port', ('type', '=',
constants.OVN_LOCALNET_VIF_PORT_TYPE))
for row in cmd.execute(check_error=True):
if (row.options and
row.options.get('network_name') == network_name):
return row.tag
def ls_has_virtual_ports(self, logical_switch):
ls = self.lookup('Logical_Switch', logical_switch)
for port in ls.ports:
if port.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE:
return True
return False
def get_nat_by_logical_port(self, logical_port):
cmd = self.db_find_rows('NAT', ('logical_port', '=', logical_port))
nat_info = cmd.execute(check_error=True)
return nat_info[0] if nat_info else []
def get_active_ports_on_chassis(self, chassis):
ports = []
cmd = self.db_find_rows('Logical_Switch_Port', ('up', '=', True))
for row in cmd.execute(check_error=True):
if (row.options and
row.options.get('requested-chassis') == chassis):
ports.append(row)
return ports
class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend): class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend):
def __init__(self, connection): def __init__(self, connection):
super(OvsdbSbOvnIdl, self).__init__(connection) super(OvsdbSbOvnIdl, self).__init__(connection)

View File

@ -231,7 +231,7 @@ class OvsIdl(object):
return self.idl_ovs.db_get( return self.idl_ovs.db_get(
'Open_vSwitch', '.', 'external_ids').execute()[key] 'Open_vSwitch', '.', 'external_ids').execute()[key]
def get_own_chassis_name(self): def get_own_chassis_id(self):
"""Return the external_ids:system-id value of the Open_vSwitch table. """Return the external_ids:system-id value of the Open_vSwitch table.
As long as ovn-controller is running on this node, the key is As long as ovn-controller is running on this node, the key is
@ -239,6 +239,14 @@ class OvsIdl(object):
""" """
return self._get_from_ext_ids('system-id') return self._get_from_ext_ids('system-id')
def get_own_chassis_name(self):
"""Return the external_ids:hostname value of the Open_vSwitch table.
As long as ovn-controller is running on this node, the key is
guaranteed to exist and will include the chassis name.
"""
return self._get_from_ext_ids('hostname')
def get_ovn_remote(self): def get_ovn_remote(self):
"""Return the external_ids:ovn-remote value of the Open_vSwitch table. """Return the external_ids:ovn-remote value of the Open_vSwitch table.

View File

@ -34,3 +34,15 @@ class OVNLBMemberEvent(row_event.RowEvent):
super(OVNLBMemberEvent, self).__init__( super(OVNLBMemberEvent, self).__init__(
events, table, None) events, table, None)
self.event_name = self.__class__.__name__ self.event_name = self.__class__.__name__
class LSPChassisEvent(row_event.RowEvent):
def __init__(self, bgp_agent, events):
self.agent = bgp_agent
table = 'Logical_Switch_Port'
super(LSPChassisEvent, self).__init__(
events, table, None)
self.event_name = self.__class__.__name__
def _check_ip_associated(self, mac):
return len(mac.strip().split(' ')) > 1

View File

@ -0,0 +1,223 @@
# Copyright 2023 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 ovn_bgp_agent import constants
from ovn_bgp_agent.drivers.openstack.watchers import base_watcher
LOG = logging.getLogger(__name__)
_SYNC_STATE_LOCK = lockutils.ReaderWriterLock()
class LogicalSwitchPortProviderCreateEvent(base_watcher.LSPChassisEvent):
def __init__(self, bgp_agent):
events = (self.ROW_UPDATE,)
super(LogicalSwitchPortProviderCreateEvent, self).__init__(
bgp_agent, events)
def match_fn(self, event, row, old):
try:
# single and dual-stack format
if not self._check_ip_associated(row.addresses[0]):
return False
current_chassis = row.options.get(constants.OVN_REQUESTED_CHASSIS)
if current_chassis != self.agent.chassis:
return False
if not row.up:
return False
old_chassis = old.options.get(constants.OVN_REQUESTED_CHASSIS)
if (not old_chassis or current_chassis != old_chassis or
not old.up):
return True
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.addresses[0].split(' ')[1:]
self.agent.expose_ip(ips, row)
class LogicalSwitchPortProviderDeleteEvent(base_watcher.LSPChassisEvent):
def __init__(self, bgp_agent):
events = (self.ROW_UPDATE, self.ROW_DELETE,)
super(LogicalSwitchPortProviderDeleteEvent, self).__init__(
bgp_agent, events)
def match_fn(self, event, row, old):
try:
# single and dual-stack format
if not self._check_ip_associated(row.addresses[0]):
return False
current_chassis = row.options.get(constants.OVN_REQUESTED_CHASSIS)
if event == self.ROW_DELETE:
return current_chassis == self.agent.chassis
# ROW_UPDATE EVENT
old_chassis = old.options.get(constants.OVN_REQUESTED_CHASSIS)
if old_chassis != self.agent.chassis:
return False
if not old.up:
return False
if (not current_chassis or current_chassis != old_chassis or
not row.up):
return True
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.addresses[0].split(' ')[1:]
self.agent.withdraw_ip(ips, row)
class LogicalSwitchPortFIPCreateEvent(base_watcher.LSPChassisEvent):
def __init__(self, bgp_agent):
events = (self.ROW_UPDATE,)
super(LogicalSwitchPortFIPCreateEvent, self).__init__(
bgp_agent, events)
def match_fn(self, event, row, old):
try:
# single and dual-stack format
if not self._check_ip_associated(row.addresses[0]):
return False
current_chassis = row.options.get(constants.OVN_REQUESTED_CHASSIS)
current_port_fip = row.external_ids.get(
constants.OVN_FIP_EXT_ID_KEY)
if (current_chassis != self.agent.chassis or not row.up or
not current_port_fip):
return False
if hasattr(old, 'options'):
# check chassis change
old_chassis = old.options.get(constants.OVN_REQUESTED_CHASSIS)
if not old_chassis or current_chassis != old_chassis:
return True
if hasattr(old, 'external_ids'):
# check fips addition
old_port_fip = old.external_ids.get(
constants.OVN_FIP_EXT_ID_KEY)
if not old_port_fip or current_port_fip != old_port_fip:
return True
if hasattr(old, 'up'):
# check port status change
if not old.up:
return True
except (IndexError, AttributeError):
return False
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
external_ip, ls_name = self.agent.get_port_external_ip_and_ls(row.name)
if not external_ip or not ls_name:
return
with _SYNC_STATE_LOCK.read_lock():
self.agent.expose_fip(external_ip, ls_name)
class LogicalSwitchPortFIPDeleteEvent(base_watcher.LSPChassisEvent):
def __init__(self, bgp_agent):
events = (self.ROW_UPDATE, self.ROW_DELETE,)
super(LogicalSwitchPortFIPDeleteEvent, self).__init__(
bgp_agent, events)
def match_fn(self, event, row, old):
try:
# single and dual-stack format
if not self._check_ip_associated(row.addresses[0]):
return False
current_chassis = row.options.get(constants.OVN_REQUESTED_CHASSIS)
current_port_fip = row.external_ids.get(
constants.OVN_FIP_EXT_ID_KEY)
if event == self.ROW_DELETE:
if (current_chassis == self.agent.chassis and row.up and
current_port_fip):
return True
return False
if hasattr(old, 'options'):
# check chassis change
old_chassis = old.options.get(constants.OVN_REQUESTED_CHASSIS)
if (not old_chassis or old_chassis != self.agent.chassis):
return False
if current_chassis != old_chassis:
return True
# There was no change in chassis, so only progress if the
# chassis matches
if current_chassis != self.agent.chassis:
return False
if hasattr(old, 'external_ids'):
# check fips deletion
old_port_fip = old.external_ids.get(
constants.OVN_FIP_EXT_ID_KEY)
if not old_port_fip:
return False
if old_port_fip != current_port_fip:
return True
if hasattr(old, 'up'):
# check port status change
if not old.up:
return False
if not row.up:
return True
except (IndexError, AttributeError):
return False
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
fip = row.external_ids.get(constants.OVN_FIP_EXT_ID_KEY)
if not fip:
fip = old.external_ids.get(constants.OVN_FIP_EXT_ID_KEY)
if not fip:
return
with _SYNC_STATE_LOCK.read_lock():
self.agent.withdraw_fip(fip)
class LocalnetCreateDeleteEvent(base_watcher.LSPChassisEvent):
def __init__(self, bgp_agent):
events = (self.ROW_CREATE, self.ROW_DELETE,)
super(LocalnetCreateDeleteEvent, self).__init__(
bgp_agent, events)
def match_fn(self, event, row, old):
if row.type == constants.OVN_LOCALNET_VIF_PORT_TYPE:
return True
return False
def run(self, event, row, old):
with _SYNC_STATE_LOCK.read_lock():
self.agent.sync()

View File

@ -0,0 +1,564 @@
# Copyright 2023 Red Hat, Inc.
# All Rights Reserved.
#
# 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 unittest import mock
from oslo_config import cfg
from ovn_bgp_agent import config
from ovn_bgp_agent import constants
from ovn_bgp_agent.drivers.openstack import nb_ovn_bgp_driver
from ovn_bgp_agent.drivers.openstack.utils import bgp as bgp_utils
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.utils import wire as wire_utils
from ovn_bgp_agent.tests import base as test_base
from ovn_bgp_agent.tests.unit import fakes
from ovn_bgp_agent.utils import linux_net
CONF = cfg.CONF
class TestNBOVNBGPDriver(test_base.TestCase):
def setUp(self):
super(TestNBOVNBGPDriver, self).setUp()
config.register_opts()
self.bridge = 'fake-bridge'
self.nb_bgp_driver = nb_ovn_bgp_driver.NBOVNBGPDriver()
self.nb_bgp_driver._post_start_event = mock.Mock()
self.nb_bgp_driver.nb_idl = mock.Mock()
self.nb_idl = self.nb_bgp_driver.nb_idl
self.nb_bgp_driver.chassis = 'fake-chassis'
self.nb_bgp_driver.ovn_bridge_mappings = {'fake-network': self.bridge}
self.mock_nbdb = mock.patch.object(ovn, 'OvnNbIdl').start()
self.mock_ovs_idl = mock.patch.object(ovs, 'OvsIdl').start()
self.nb_bgp_driver.ovs_idl = self.mock_ovs_idl
self.ipv4 = '192.168.1.17'
self.ipv6 = '2002::1234:abcd:ffff:c0a8:101'
self.fip = '172.24.4.33'
self.mac = 'aa:bb:cc:dd:ee:ff'
self.ovn_routing_tables = {
self.bridge: 100,
'br-vlan': 200}
self.nb_bgp_driver.ovn_routing_tables = self.ovn_routing_tables
self.ovn_routing_tables_routes = mock.Mock()
self.nb_bgp_driver.ovn_routing_tables_routes = (
self.ovn_routing_tables_routes)
self.conf_ovsdb_connection = 'tcp:127.0.0.1:6642'
# Mock pyroute2.NDB context manager object
self.mock_ndb = mock.patch.object(linux_net.pyroute2, 'NDB').start()
self.fake_ndb = self.mock_ndb().__enter__()
@mock.patch.object(linux_net, 'ensure_vrf')
@mock.patch.object(frr, 'vrf_leak')
@mock.patch.object(linux_net, 'ensure_ovn_device')
@mock.patch.object(linux_net, 'delete_routes_from_table')
def test_start(self, mock_delete_routes_from_table,
mock_ensure_ovn_device, mock_vrf_leak, mock_ensure_vrf):
CONF.set_override('clear_vrf_routes_on_startup', True)
self.addCleanup(CONF.clear_override, 'clear_vrf_routes_on_startup')
self.mock_ovs_idl.get_own_chassis_name.return_value = 'chassis-name'
self.mock_ovs_idl.get_ovn_remote.return_value = (
self.conf_ovsdb_connection)
self.nb_bgp_driver.start()
# Verify mock object method calls and arguments
self.mock_ovs_idl().start.assert_called_once_with(
CONF.ovsdb_connection)
self.mock_ovs_idl().get_own_chassis_name.assert_called_once()
self.mock_ovs_idl().get_ovn_remote.assert_called_once()
mock_ensure_vrf.assert_called_once_with(
CONF.bgp_vrf, CONF.bgp_vrf_table_id)
mock_vrf_leak.assert_called_once_with(
CONF.bgp_vrf, CONF.bgp_AS, CONF.bgp_router_id)
mock_ensure_ovn_device.assert_called_once_with(CONF.bgp_nic,
CONF.bgp_vrf)
mock_delete_routes_from_table.assert_called_once_with(
CONF.bgp_vrf_table_id)
self.mock_nbdb().start.assert_called_once_with()
@mock.patch.object(linux_net, 'delete_bridge_ip_routes')
@mock.patch.object(linux_net, 'delete_ip_rules')
@mock.patch.object(linux_net, 'delete_exposed_ips')
@mock.patch.object(linux_net, 'get_ovn_ip_rules')
@mock.patch.object(linux_net, 'get_exposed_ips')
@mock.patch.object(ovs, 'remove_extra_ovs_flows')
@mock.patch.object(ovs, 'get_ovs_flows_info')
@mock.patch.object(linux_net, 'ensure_arp_ndp_enabled_for_bridge')
@mock.patch.object(linux_net, 'ensure_vlan_device_for_network')
@mock.patch.object(linux_net, 'ensure_routing_table_for_bridge')
@mock.patch.object(linux_net, 'ensure_ovn_device')
@mock.patch.object(linux_net, 'ensure_vrf')
def test_sync(self, mock_ensure_vrf, mock_ensure_ovn_dev,
mock_routing_bridge, mock_ensure_vlan_network,
mock_ensure_arp, mock_flows_info, mock_remove_flows,
mock_exposed_ips, mock_get_ip_rules, mock_del_exposed_ips,
mock_del_ip_riles, moock_del_ip_routes):
self.mock_ovs_idl.get_ovn_bridge_mappings.return_value = [
'net0:bridge0', 'net1:bridge1']
self.nb_idl.get_network_vlan_tag_by_network_name.side_effect = (
[10], [11])
fake_ip_rules = 'fake-ip-rules'
mock_get_ip_rules.return_value = fake_ip_rules
ips = [self.ipv4, self.ipv6]
mock_exposed_ips.return_value = ips
port0 = fakes.create_object({
'name': 'port-0',
'type': constants.OVN_VM_VIF_PORT_TYPE})
port1 = fakes.create_object({
'name': 'port-1',
'type': constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE})
self.nb_idl.get_active_ports_on_chassis.return_value = [
port0, port1]
mock_ensure_port_exposed = mock.patch.object(
self.nb_bgp_driver, '_ensure_port_exposed').start()
self.nb_bgp_driver.sync()
mock_ensure_vrf.assert_called_once_with(
CONF.bgp_vrf, CONF.bgp_vrf_table_id)
mock_ensure_ovn_dev.assert_called_once_with(
CONF.bgp_nic, CONF.bgp_vrf)
expected_calls = [mock.call(self.ovn_routing_tables, 'bridge0',
CONF.bgp_vrf_table_id),
mock.call(self.ovn_routing_tables, 'bridge1',
CONF.bgp_vrf_table_id)]
mock_routing_bridge.assert_has_calls(expected_calls)
expected_calls = [mock.call('bridge0', 10), mock.call('bridge1', 11)]
mock_ensure_vlan_network.assert_has_calls(expected_calls)
expected_calls = [mock.call('bridge0', 1, 10),
mock.call('bridge1', 2, 11)]
mock_ensure_arp.assert_has_calls(expected_calls)
expected_calls = [
mock.call(
'bridge0', {'bridge0': {'mac': mock.ANY, 'in_port': set()},
'bridge1': {'mac': mock.ANY, 'in_port': set()}},
constants.OVS_RULE_COOKIE),
mock.call(
'bridge1', {'bridge0': {'mac': mock.ANY, 'in_port': set()},
'bridge1': {'mac': mock.ANY, 'in_port': set()}},
constants.OVS_RULE_COOKIE)]
mock_flows_info.assert_has_calls(expected_calls)
mock_remove_flows.assert_called_once_with({
'bridge0': {'mac': mock.ANY, 'in_port': set()},
'bridge1': {'mac': mock.ANY, 'in_port': set()}},
constants.OVS_RULE_COOKIE)
mock_get_ip_rules.assert_called_once()
mock_ensure_port_exposed.assert_called_once_with(
port0, ips, fake_ip_rules)
mock_del_exposed_ips.assert_called_once_with(
ips, CONF.bgp_nic)
mock_del_ip_riles.assert_called_once_with(fake_ip_rules)
moock_del_ip_routes.assert_called_once_with(
self.ovn_routing_tables, mock.ANY,
{'bridge0': mock.ANY, 'bridge1': mock.ANY})
def test__ensure_port_exposed_fip(self):
port0 = fakes.create_object({
'name': 'port-0',
'external_ids': {constants.OVN_FIP_EXT_ID_KEY: "fip"}})
exposed_ips = ["192.168.0.10"]
ovn_ip_rules = {"192.168.0.10/32": "rule1"}
mock_get_port_external_ip_and_ls = mock.patch.object(
self.nb_bgp_driver, 'get_port_external_ip_and_ls').start()
mock_get_port_external_ip_and_ls.return_value = ("192.168.0.10",
"test-ls")
mock_expose_fip = mock.patch.object(
self.nb_bgp_driver, '_expose_fip').start()
mock_expose_ip = mock.patch.object(
self.nb_bgp_driver, '_expose_ip').start()
self.nb_bgp_driver._ensure_port_exposed(port0, exposed_ips,
ovn_ip_rules)
mock_get_port_external_ip_and_ls.assert_called_once_with(port0.name)
mock_expose_fip.assert_called_once_with("192.168.0.10", "test-ls")
mock_expose_ip.assert_not_called()
self.assertEqual(exposed_ips, [])
self.assertEqual(ovn_ip_rules, {})
def test__ensure_port_exposed_tenant_ls(self):
port0 = fakes.create_object({
'name': 'port-0',
'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: "test-ls"}})
self.nb_bgp_driver.ovn_tenant_ls = {"test-ls": True}
exposed_ips = ["192.168.0.10"]
ovn_ip_rules = {"192.168.0.10/32": "rule1"}
mock_get_port_external_ip_and_ls = mock.patch.object(
self.nb_bgp_driver, 'get_port_external_ip_and_ls').start()
mock_expose_fip = mock.patch.object(
self.nb_bgp_driver, '_expose_fip').start()
mock_expose_ip = mock.patch.object(
self.nb_bgp_driver, '_expose_ip').start()
self.nb_bgp_driver._ensure_port_exposed(port0, exposed_ips,
ovn_ip_rules)
mock_get_port_external_ip_and_ls.assert_not_called()
mock_expose_fip.assert_not_called()
mock_expose_ip.assert_not_called()
self.assertEqual(exposed_ips, ["192.168.0.10"])
self.assertEqual(ovn_ip_rules, {"192.168.0.10/32": "rule1"})
@mock.patch.object(linux_net, 'get_ip_version')
def test__ensure_port_exposed_no_fip_no_tenant_ls(self, mock_ip_version):
port0 = fakes.create_object({
'name': 'port-0',
'addresses': ["fake_mac 192.168.0.10"],
'type': constants.OVN_VM_VIF_PORT_TYPE,
'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: "test-ls"}})
self.nb_bgp_driver.ovn_tenant_ls = {}
self.nb_bgp_driver.ovn_provider_ls = {}
exposed_ips = ["192.168.0.10"]
ovn_ip_rules = {"192.168.0.10/32": "rule1"}
mock_get_port_external_ip_and_ls = mock.patch.object(
self.nb_bgp_driver, 'get_port_external_ip_and_ls').start()
mock_expose_fip = mock.patch.object(
self.nb_bgp_driver, '_expose_fip').start()
mock_expose_ip = mock.patch.object(
self.nb_bgp_driver, '_expose_ip').start()
mock_expose_ip.return_value = ["192.168.0.10"]
mock_get_ls_localnet_info = mock.patch.object(
self.nb_bgp_driver, '_get_ls_localnet_info').start()
mock_get_ls_localnet_info.return_value = ("br-ex", 10)
mock_ip_version.return_value = constants.IP_VERSION_4
self.nb_bgp_driver._ensure_port_exposed(port0, exposed_ips,
ovn_ip_rules)
mock_get_port_external_ip_and_ls.assert_not_called()
mock_get_ls_localnet_info.assert_called_once_with('test-ls')
mock_expose_fip.assert_not_called()
mock_expose_ip.assert_called_once_with(["192.168.0.10"], "br-ex", 10,
constants.OVN_VM_VIF_PORT_TYPE,
None)
self.assertEqual(exposed_ips, [])
self.assertEqual(ovn_ip_rules, {})
@mock.patch.object(wire_utils, 'wire_provider_port')
@mock.patch.object(bgp_utils, 'announce_ips')
def test__expose_provider_port_successful(self, mock_announce_ips,
mock_wire_provider_port):
mock_wire_provider_port.return_value = True
port_ips = ['192.168.0.1', '192.168.0.2']
bridge_device = self.bridge
bridge_vlan = None
proxy_cidrs = ['192.168.0.0/24']
self.nb_bgp_driver._expose_provider_port(port_ips, bridge_device,
bridge_vlan, proxy_cidrs)
mock_wire_provider_port.assert_called_once_with(
self.ovn_routing_tables_routes, port_ips, bridge_device,
bridge_vlan, self.ovn_routing_tables[bridge_device], proxy_cidrs)
mock_announce_ips.assert_called_once_with(port_ips)
@mock.patch.object(wire_utils, 'wire_provider_port')
@mock.patch.object(bgp_utils, 'announce_ips')
def test__expose_provider_port_failure(self, mock_announce_ips,
mock_wire_provider_port):
mock_wire_provider_port.return_value = False
port_ips = ['192.168.0.1', '192.168.0.2']
bridge_device = self.bridge
bridge_vlan = None
proxy_cidrs = ['192.168.0.0/24']
self.nb_bgp_driver._expose_provider_port(port_ips, bridge_device,
bridge_vlan, proxy_cidrs)
mock_wire_provider_port.assert_called_once_with(
self.ovn_routing_tables_routes, port_ips, bridge_device,
bridge_vlan, self.ovn_routing_tables[bridge_device], proxy_cidrs)
mock_announce_ips.assert_not_called()
@mock.patch.object(wire_utils, 'unwire_provider_port')
@mock.patch.object(bgp_utils, 'withdraw_ips')
def test__withdraw_provider_port(self, mock_withdraw_ips,
mock_unwire_provider_port):
port_ips = ['192.168.0.1', '192.168.0.2']
bridge_device = self.bridge
bridge_vlan = None
proxy_cidrs = ['192.168.0.0/24']
self.nb_bgp_driver._withdraw_provider_port(port_ips, bridge_device,
bridge_vlan, proxy_cidrs)
mock_withdraw_ips.assert_called_once_with(port_ips)
mock_unwire_provider_port.assert_called_once_with(
self.ovn_routing_tables_routes, port_ips, bridge_device,
bridge_vlan, self.ovn_routing_tables[bridge_device], proxy_cidrs)
def test__get_bridge_for_localnet_port(self):
localnet = fakes.create_object({
'options': {'network_name': 'fake-network'},
'tag': [10]})
bridge_device, bridge_vlan = (
self.nb_bgp_driver._get_bridge_for_localnet_port(localnet))
self.assertEqual(bridge_device, self.bridge)
self.assertEqual(bridge_vlan, 10)
def test__get_bridge_for_localnet_port_no_network_no_tag(self):
localnet = fakes.create_object({
'options': {},
'tag': None})
bridge_device, bridge_vlan = (
self.nb_bgp_driver._get_bridge_for_localnet_port(localnet))
self.assertEqual(bridge_device, None)
self.assertEqual(bridge_vlan, None)
def _test_expose_ip(self, ips, row):
mock_expose_provider_port = mock.patch.object(
self.nb_bgp_driver, '_expose_provider_port').start()
mock_get_ls_localnet_info = mock.patch.object(
self.nb_bgp_driver, '_get_ls_localnet_info').start()
mock_get_ls_localnet_info.return_value = ('br-ex', 10)
cidr = row.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY)
logical_switch = row.external_ids.get(constants.OVN_LS_NAME_EXT_ID_KEY)
self.nb_bgp_driver.expose_ip(ips, row)
if not logical_switch:
mock_expose_provider_port.assert_not_called()
mock_get_ls_localnet_info.assert_not_called()
return
mock_get_ls_localnet_info.assert_called_once_with(logical_switch)
self.assertEqual(self.nb_bgp_driver.ovn_provider_ls[logical_switch],
{'bridge_device': 'br-ex', 'bridge_vlan': 10})
if row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE and cidr:
mock_expose_provider_port.assert_called_once_with(ips, 'br-ex',
10, [cidr])
else:
mock_expose_provider_port.assert_called_once_with(ips, 'br-ex', 10)
def test_expose_ip(self):
ips = [self.ipv4, self.ipv6]
row = fakes.create_object({
'type': constants.OVN_VM_VIF_PORT_TYPE,
'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}})
self._test_expose_ip(ips, row)
def test_expose_ip_virtual(self):
ips = [self.ipv4, self.ipv6]
row = fakes.create_object({
'type': constants.OVN_VIRTUAL_VIF_PORT_TYPE,
'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls',
constants.OVN_CIDRS_EXT_ID_KEY: 'test-cidr'}})
self._test_expose_ip(ips, row)
def test_expose_ip_no_switch(self):
ips = [self.ipv4, self.ipv6]
row = fakes.create_object({
'type': constants.OVN_VM_VIF_PORT_TYPE,
'external_ids': {}})
self._test_expose_ip(ips, row)
@mock.patch.object(linux_net, 'get_ip_version')
def _test_withdraw_ip(self, ips, row, provider, mock_ip_version):
mock_withdraw_provider_port = mock.patch.object(
self.nb_bgp_driver, '_withdraw_provider_port').start()
mock_get_ls_localnet_info = mock.patch.object(
self.nb_bgp_driver, '_get_ls_localnet_info').start()
mock_ip_version.return_value = constants.IP_VERSION_6
self.nb_idl.ls_has_virtual_ports.return_value = False
if provider:
mock_get_ls_localnet_info.return_value = ('br-ex', 10)
else:
mock_get_ls_localnet_info.return_value = (None, None)
cidr = row.external_ids.get(constants.OVN_CIDRS_EXT_ID_KEY)
logical_switch = row.external_ids.get(constants.OVN_LS_NAME_EXT_ID_KEY)
self.nb_bgp_driver.withdraw_ip(ips, row)
if not logical_switch:
mock_get_ls_localnet_info.assert_not_called()
mock_withdraw_provider_port.assert_not_called()
return
if not provider:
mock_get_ls_localnet_info.assert_called_once_with(logical_switch)
mock_withdraw_provider_port.assert_not_called()
return
mock_get_ls_localnet_info.assert_called_once_with(logical_switch)
if row.type == constants.OVN_VIRTUAL_VIF_PORT_TYPE and cidr:
mock_withdraw_provider_port.assert_called_once_with(ips, 'br-ex',
10, [cidr])
else:
mock_withdraw_provider_port.assert_called_once_with(ips, 'br-ex',
10)
def test_withdraw_ip(self):
ips = [self.ipv4, self.ipv6]
row = fakes.create_object({
'type': constants.OVN_VM_VIF_PORT_TYPE,
'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}})
self._test_withdraw_ip(ips, row, True)
def test_withdraw_ip_no_provider(self):
ips = [self.ipv4, self.ipv6]
row = fakes.create_object({
'type': constants.OVN_VM_VIF_PORT_TYPE,
'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls'}})
self._test_withdraw_ip(ips, row, False)
def test_withdraw_ip_virtual(self):
ips = [self.ipv4, self.ipv6]
row = fakes.create_object({
'type': constants.OVN_VIRTUAL_VIF_PORT_TYPE,
'external_ids': {constants.OVN_LS_NAME_EXT_ID_KEY: 'test-ls',
constants.OVN_CIDRS_EXT_ID_KEY: 'test-cidr'}})
self._test_withdraw_ip(ips, row, True)
def test_withdraw_ip_no_switch(self):
ips = [self.ipv4, self.ipv6]
row = fakes.create_object({
'type': constants.OVN_VM_VIF_PORT_TYPE,
'external_ids': {}})
self._test_withdraw_ip(ips, row, True)
def test__get_ls_localnet_info(self):
logical_switch = 'lswitch1'
localnet_ports = ['fake-localnet-port']
self.nb_idl.ls_get_localnet_ports.return_value.execute.return_value = (
localnet_ports)
mock_get_bridge_for_localnet_port = mock.patch.object(
self.nb_bgp_driver, '_get_bridge_for_localnet_port').start()
self.nb_bgp_driver._get_ls_localnet_info(logical_switch)
self.nb_idl.ls_get_localnet_ports.assert_called_once_with(
logical_switch, if_exists=True)
mock_get_bridge_for_localnet_port.assert_called_once_with(
localnet_ports[0])
def test_get_ls_localnet_info_not_provider_network(self):
logical_switch = 'lswitch1'
localnet_ports = []
self.nb_idl.ls_get_localnet_ports.return_value.execute.return_value = (
localnet_ports)
mock_get_bridge_for_localnet_port = mock.patch.object(
self.nb_bgp_driver, '_get_bridge_for_localnet_port').start()
ret = self.nb_bgp_driver._get_ls_localnet_info(logical_switch)
self.nb_idl.ls_get_localnet_ports.assert_called_once_with(
logical_switch, if_exists=True)
mock_get_bridge_for_localnet_port.assert_not_called()
self.assertEqual(ret, (None, None))
def test_get_port_external_ip_and_ls(self):
nat_entry = fakes.create_object({
'external_ids': {constants.OVN_FIP_NET_EXT_ID_KEY: 'net1'},
'external_ip': 'fake-ip'})
self.nb_idl.get_nat_by_logical_port.return_value = nat_entry
ret = self.nb_bgp_driver.get_port_external_ip_and_ls('fake-port')
expected_result = (nat_entry.external_ip, "neutron-net1")
self.assertEqual(ret, expected_result)
def test_get_port_external_ip_and_ls_no_nat_entry(self):
self.nb_idl.get_nat_by_logical_port.return_value = None
ret = self.nb_bgp_driver.get_port_external_ip_and_ls('fake-port')
self.assertIsNone(ret)
def test_get_port_external_ip_and_ls_no_external_id(self):
nat_entry = fakes.create_object({
'external_ids': {},
'external_ip': 'fake-ip'})
self.nb_idl.get_nat_by_logical_port.return_value = nat_entry
ret = self.nb_bgp_driver.get_port_external_ip_and_ls('fake-port')
self.assertEqual(ret, (nat_entry.external_ip, None))
def test_expose_fip(self):
ip = '10.0.0.1'
logical_switch = 'lswitch1'
mock_get_ls_localnet_info = mock.patch.object(
self.nb_bgp_driver, '_get_ls_localnet_info').start()
mock_get_ls_localnet_info.return_value = ('br-ex', 100)
mock_expose_provider_port = mock.patch.object(
self.nb_bgp_driver, '_expose_provider_port').start()
ret = self.nb_bgp_driver.expose_fip(ip, logical_switch)
mock_get_ls_localnet_info.assert_called_once_with(logical_switch)
mock_expose_provider_port.assert_called_once_with([ip], 'br-ex', 100)
self.assertEqual(self.nb_bgp_driver.ovn_fips[ip],
{'bridge_device': 'br-ex', 'bridge_vlan': 100})
self.assertTrue(ret)
def test_expose_fip_no_device(self):
ip = '10.0.0.1'
logical_switch = 'lswitch1'
mock_get_ls_localnet_info = mock.patch.object(
self.nb_bgp_driver, '_get_ls_localnet_info').start()
mock_get_ls_localnet_info.return_value = (None, None)
mock_expose_provider_port = mock.patch.object(
self.nb_bgp_driver, '_expose_provider_port').start()
ret = self.nb_bgp_driver.expose_fip(ip, logical_switch)
mock_get_ls_localnet_info.assert_called_once_with(logical_switch)
mock_expose_provider_port.assert_not_called()
self.assertNotIn(ip, self.nb_bgp_driver.ovn_fips)
self.assertFalse(ret)
def test_withdraw_fip(self):
ip = '10.0.0.1'
self.nb_bgp_driver.ovn_fips = {ip: {'bridge_device': 'br-ex',
'bridge_vlan': 100}}
mock_withdraw_provider_port = mock.patch.object(
self.nb_bgp_driver, '_withdraw_provider_port').start()
self.nb_bgp_driver.withdraw_fip(ip)
mock_withdraw_provider_port.assert_called_once_with([ip], "br-ex", 100)
def test_withdraw_fip_not_found(self):
ip = '10.0.0.1'
self.nb_bgp_driver.ovn_fips = {}
mock_withdraw_provider_port = mock.patch.object(
self.nb_bgp_driver, '_withdraw_provider_port').start()
self.nb_bgp_driver.withdraw_fip(ip)
mock_withdraw_provider_port.assert_not_called()

View File

@ -30,6 +30,79 @@ from ovn_bgp_agent.tests.unit import fakes
CONF = cfg.CONF CONF = cfg.CONF
class TestOvsdbNbOvnIdl(test_base.TestCase):
def setUp(self):
super(TestOvsdbNbOvnIdl, self).setUp()
self.nb_idl = ovn_utils.OvsdbNbOvnIdl(mock.Mock())
# Monkey-patch parent class methods
self.nb_idl.db_find_rows = mock.Mock()
self.nb_idl.lookup = mock.Mock()
def test_get_network_vlan_tag_by_network_name(self):
network_name = 'net0'
tag = 123
lsp = fakes.create_object({'name': 'port-0',
'options': {'network_name': network_name},
'tag': tag})
self.nb_idl.db_find_rows.return_value.execute.return_value = [
lsp]
ret = self.nb_idl.get_network_vlan_tag_by_network_name(network_name)
self.assertEqual(tag, ret)
self.nb_idl.db_find_rows.assert_called_once_with(
'Logical_Switch_Port',
('type', '=', constants.OVN_LOCALNET_VIF_PORT_TYPE))
def test_ls_has_virtual_ports(self):
ls_name = 'logical_switch'
port = fakes.create_object(
{'type': constants.OVN_VIRTUAL_VIF_PORT_TYPE})
ls = fakes.create_object({'ports': [port]})
self.nb_idl.lookup.return_value = ls
ret = self.nb_idl.ls_has_virtual_ports(ls_name)
self.assertEqual(True, ret)
self.nb_idl.lookup.assert_called_once_with('Logical_Switch', ls_name)
def test_ls_has_virtual_ports_not_found(self):
ls_name = 'logical_switch'
port = fakes.create_object({'type': constants.OVN_VM_VIF_PORT_TYPE})
ls = fakes.create_object({'ports': [port]})
self.nb_idl.lookup.return_value = ls
ret = self.nb_idl.ls_has_virtual_ports(ls_name)
self.assertEqual(False, ret)
self.nb_idl.lookup.assert_called_once_with('Logical_Switch', ls_name)
def test_get_nat_by_logical_port(self):
logical_port = 'logical_port'
nat_info = ['nat_info']
self.nb_idl.db_find_rows.return_value.execute.return_value = nat_info
ret = self.nb_idl.get_nat_by_logical_port(logical_port)
self.assertEqual('nat_info', ret)
self.nb_idl.db_find_rows.assert_called_once_with(
'NAT',
('logical_port', '=', logical_port))
def test_get_active_ports_on_chassis(self):
chassis = 'local_chassis'
row1 = fakes.create_object({
'options': {'requested-chassis': chassis}})
row2 = fakes.create_object({
'options': {'requested-chassis': 'other_chassis'}})
self.nb_idl.db_find_rows.return_value.execute.return_value = [
row1, row2]
ret = self.nb_idl.get_active_ports_on_chassis(chassis)
self.assertEqual([row1], ret)
self.nb_idl.db_find_rows.assert_called_once_with(
'Logical_Switch_Port',
('up', '=', True))
class TestOvsdbSbOvnIdl(test_base.TestCase): class TestOvsdbSbOvnIdl(test_base.TestCase):
def setUp(self): def setUp(self):
@ -561,6 +634,43 @@ class TestOvsdbSbOvnIdl(test_base.TestCase):
self.assertEqual(lb2, ret) self.assertEqual(lb2, ret)
class TestOvnNbIdl(test_base.TestCase):
def setUp(self):
super(TestOvnNbIdl, self).setUp()
config.register_opts()
mock.patch.object(idlutils, 'get_schema_helper').start()
mock.patch.object(ovn_utils.OvnIdl, '__init__').start()
self.nb_idl = ovn_utils.OvnNbIdl('tcp:127.0.0.1:6640')
@mock.patch.object(Stream, 'ssl_set_ca_cert_file')
@mock.patch.object(Stream, 'ssl_set_certificate_file')
@mock.patch.object(Stream, 'ssl_set_private_key_file')
def test__check_and_set_ssl_files(
self, mock_ssl_priv_key, mock_ssl_cert, mock_ssl_ca_cert):
CONF.set_override('ovn_nb_private_key', 'fake-priv-key')
CONF.set_override('ovn_nb_certificate', 'fake-cert')
CONF.set_override('ovn_nb_ca_cert', 'fake-ca-cert')
self.nb_idl._check_and_set_ssl_files('fake-schema')
mock_ssl_priv_key.assert_called_once_with('fake-priv-key')
mock_ssl_cert.assert_called_once_with('fake-cert')
mock_ssl_ca_cert.assert_called_once_with('fake-ca-cert')
@mock.patch.object(connection, 'Connection')
def test_start(self, mock_conn):
notify_handler = mock.Mock()
self.nb_idl.notify_handler = notify_handler
self.nb_idl._events = ['fake-event0', 'fake-event1']
self.nb_idl.start()
mock_conn.assert_called_once_with(self.nb_idl, timeout=180)
notify_handler.watch_events.assert_called_once_with(
['fake-event0', 'fake-event1'])
class TestOvnSbIdl(test_base.TestCase): class TestOvnSbIdl(test_base.TestCase):
def setUp(self): def setUp(self):

View File

@ -405,11 +405,11 @@ class TestOvsIdl(test_base.TestCase):
self.ovs_idl.idl_ovs.db_get.assert_called_once_with( self.ovs_idl.idl_ovs.db_get.assert_called_once_with(
'Open_vSwitch', '.', 'external_ids') 'Open_vSwitch', '.', 'external_ids')
def test_get_own_chassis_name(self): def test_get_own_chassis_id(self):
expected_return = 'fake-sys' expected_return = 'fake-sys'
row = {'system-id': expected_return} row = {'system-id': expected_return}
self._test_ovs_ext_ids_getters( self._test_ovs_ext_ids_getters(
self.ovs_idl.get_own_chassis_name, row, expected_return) self.ovs_idl.get_own_chassis_id, row, expected_return)
def test_get_ovn_remote(self): def test_get_ovn_remote(self):
expected_return = 'fake-ovn-remote' expected_return = 'fake-ovn-remote'

View File

@ -0,0 +1,406 @@
# Copyright 2023 Red Hat, Inc.
# All Rights Reserved.
#
# 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 unittest import mock
from oslo_config import cfg
from ovn_bgp_agent import constants
from ovn_bgp_agent.drivers.openstack.watchers import nb_bgp_watcher
from ovn_bgp_agent.tests import base as test_base
from ovn_bgp_agent.tests import utils
CONF = cfg.CONF
class TestLogicalSwitchPortProviderCreateEvent(test_base.TestCase):
def setUp(self):
super(TestLogicalSwitchPortProviderCreateEvent, self).setUp()
self.chassis = 'fake-chassis'
self.agent = mock.Mock(chassis=self.chassis)
self.event = nb_bgp_watcher.LogicalSwitchPortProviderCreateEvent(
self.agent)
def test_match_fn(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
up=True)
old = utils.create_row(options={}, up=True)
self.assertTrue(self.event.match_fn(mock.Mock(), row, old))
def test_match_fn_exception(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
up=False)
self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock()))
def test_match_fn_not_up(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
up=False)
self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock()))
def test_match_fn_invalid_address(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac '],
options={'requested-chassis': self.chassis},
up=True)
self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock()))
def test_match_fn_wrong_chassis(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': 'fake_chassis'},
up=True)
old = utils.create_row(options={}, up=True)
self.assertFalse(self.event.match_fn(mock.Mock(), row, old))
def test_run(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
up=True)
self.event.run(mock.Mock(), row, mock.Mock())
self.agent.expose_ip.assert_called_once_with(['192.168.0.1'], row)
def test_run_wrong_type(self):
row = utils.create_row(
type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
up=True)
self.event.run(mock.Mock(), row, mock.Mock())
self.agent.expose_ip.assert_not_called()
class TestLogicalSwitchPortProviderDeleteEvent(test_base.TestCase):
def setUp(self):
super(TestLogicalSwitchPortProviderDeleteEvent, self).setUp()
self.chassis = 'fake-chassis'
self.agent = mock.Mock(chassis=self.chassis)
self.event = nb_bgp_watcher.LogicalSwitchPortProviderDeleteEvent(
self.agent)
def test_match_fn_delete(self):
event = self.event.ROW_DELETE
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
up=True)
self.assertTrue(self.event.match_fn(event, row, mock.Mock()))
def test_match_fn_update(self):
event = self.event.ROW_UPDATE
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
up=False)
old = utils.create_row(options={'requested-chassis': self.chassis},
up=True)
self.assertTrue(self.event.match_fn(event, row, old))
def test_match_fn_exception(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
up=False)
self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock()))
def test_match_fn_not_up(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
up=False)
old = utils.create_row(options={'requested-chassis': self.chassis},
up=False)
self.assertFalse(self.event.match_fn(mock.Mock(), row, old))
def test_match_fn_invalid_address(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac '],
options={'requested-chassis': self.chassis},
up=True)
self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock()))
def test_match_fn_wrong_chassis(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
up=True)
old = utils.create_row(options={'requested-chassis': 'other_chassis'})
self.assertFalse(self.event.match_fn(mock.Mock(), row, old))
def test_run(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
up=True)
self.event.run(mock.Mock(), row, mock.Mock())
self.agent.withdraw_ip.assert_called_once_with(['192.168.0.1'], row)
def test_run_wrong_type(self):
row = utils.create_row(
type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
up=True)
self.event.run(mock.Mock(), row, mock.Mock())
self.agent.withdraw_ip.assert_not_called()
class TestLogicalSwitchPortFIPCreateEvent(test_base.TestCase):
def setUp(self):
super(TestLogicalSwitchPortFIPCreateEvent, self).setUp()
self.chassis = 'fake-chassis'
self.agent = mock.Mock(chassis=self.chassis)
self.event = nb_bgp_watcher.LogicalSwitchPortFIPCreateEvent(
self.agent)
def test_match_fn_chassis_change(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
external_ids={
constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'},
up=True)
old = utils.create_row(options={}, up=True)
self.assertTrue(self.event.match_fn(mock.Mock(), row, old))
def test_match_fn_status_change(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
external_ids={
constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'},
up=True)
old = utils.create_row(options={'requested-chassis': self.chassis},
external_ids={
constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'},
up=False)
self.assertTrue(self.event.match_fn(mock.Mock(), row, old))
def test_match_fn_fip_addition(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
external_ids={
constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'},
up=True)
old = utils.create_row(options={'requested-chassis': self.chassis},
external_ids={},
up=True)
self.assertTrue(self.event.match_fn(mock.Mock(), row, old))
def test_match_fn_no_fip(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
external_ids={},
up=True)
self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock()))
def test_match_fn_wrong_chassis(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': 'wrong_chassis'},
external_ids={
constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'},
up=True)
self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock()))
def test_match_fn_port_down(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
external_ids={
constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'},
up=False)
self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock()))
def test_match_fn_wrong_address(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac '],
options={'requested-chassis': self.chassis},
external_ids={
constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'},
up=True)
self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock()))
def test_match_fn_exception(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
up=False)
self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock()))
def test_run(self):
external_ip = '10.0.0.10'
ls_name = 'neutron-net-id'
self.agent.get_port_external_ip_and_ls.return_value = (external_ip,
ls_name)
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
name='net-id')
self.event.run(mock.Mock(), row, mock.Mock())
self.agent.expose_fip.assert_called_once_with(external_ip, ls_name)
def test_run_no_external_ip(self):
external_ip = None
ls_name = 'logical_switch'
self.agent.get_port_external_ip_and_ls.return_value = (external_ip,
ls_name)
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
name='net-id')
self.event.run(mock.Mock(), row, mock.Mock())
self.agent.expose_fip.assert_not_called()
def test_run_wrong_type(self):
row = utils.create_row(
type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE)
self.event.run(mock.Mock(), row, mock.Mock())
self.agent.expose_fip.assert_not_called()
class TestLogicalSwitchPortFIPDeleteEvent(test_base.TestCase):
def setUp(self):
super(TestLogicalSwitchPortFIPDeleteEvent, self).setUp()
self.chassis = 'fake-chassis'
self.agent = mock.Mock(chassis=self.chassis)
self.event = nb_bgp_watcher.LogicalSwitchPortFIPDeleteEvent(
self.agent)
def test_match_fn_delete(self):
event = self.event.ROW_DELETE
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
external_ids={
constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'},
up=True)
self.assertTrue(self.event.match_fn(event, row, mock.Mock()))
def test_match_fn_update(self):
event = self.event.ROW_UPDATE
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
external_ids={
constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'},
up=False)
old = utils.create_row(up=True)
self.assertTrue(self.event.match_fn(event, row, old))
def test_match_fn_exception(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
up=False)
self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock()))
def test_match_fn_not_up(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
external_ids={
constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'},
up=False)
old = utils.create_row(options={'requested-chassis': self.chassis},
up=False)
self.assertFalse(self.event.match_fn(mock.Mock(), row, old))
def test_match_fn_invalid_address(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac '],
options={'requested-chassis': self.chassis},
up=True)
self.assertFalse(self.event.match_fn(mock.Mock(), row, mock.Mock()))
def test_match_fn_wrong_chassis(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
external_ids={
constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'},
up=True)
old = utils.create_row(options={'requested-chassis': 'other_chassis'})
self.assertFalse(self.event.match_fn(mock.Mock(), row, old))
def test_match_fn_chassis_update(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': 'other_chassis'},
external_ids={
constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'},
up=True)
old = utils.create_row(options={'requested-chassis': self.chassis})
self.assertTrue(self.event.match_fn(mock.Mock(), row, old))
def test_match_fn_fip_update(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
addresses=['mac 192.168.0.1'],
options={'requested-chassis': self.chassis},
external_ids={
constants.OVN_FIP_EXT_ID_KEY: 'new-fip-ip'},
up=True)
old = utils.create_row(
external_ids={constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'})
self.assertTrue(self.event.match_fn(mock.Mock(), row, old))
def test_run(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
external_ids={
constants.OVN_FIP_EXT_ID_KEY: 'fip-ip'},
up=True)
self.event.run(mock.Mock(), row, mock.Mock())
self.agent.withdraw_fip.assert_called_once_with('fip-ip')
def test_run_no_fip(self):
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
external_ids={})
old = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE,
external_ids={})
self.event.run(mock.Mock(), row, old)
self.agent.withdraw_fip.assert_not_called()
def test_run_wrong_type(self):
row = utils.create_row(
type=constants.OVN_CHASSISREDIRECT_VIF_PORT_TYPE)
self.event.run(mock.Mock(), row, mock.Mock())
self.agent.withdraw_fip.assert_not_called()
class TestLocalnetCreateDeleteEvent(test_base.TestCase):
def setUp(self):
super(TestLocalnetCreateDeleteEvent, self).setUp()
self.agent = mock.Mock()
self.event = nb_bgp_watcher.LocalnetCreateDeleteEvent(
self.agent)
def test_match_fn(self):
row = utils.create_row(type=constants.OVN_LOCALNET_VIF_PORT_TYPE)
self.assertTrue(self.event.match_fn(None, row, None))
row = utils.create_row(type=constants.OVN_VM_VIF_PORT_TYPE)
self.assertFalse(self.event.match_fn(None, row, None))
def test_run(self):
row = utils.create_row(type=constants.OVN_LOCALNET_VIF_PORT_TYPE)
self.event.run(None, row, None)
self.agent.sync.assert_called_once()

View File

@ -0,0 +1,9 @@
---
features:
- |
This patch introduces a new driver that instead of connecting to the OVN
SB DB to watch for relevant events, it connects to the OVN NB DB. The main
reasons for doing so are: 1) scalability purposes; and 2) rely on the
stable fields offered by the NB DB, instead of the SB DB that may change
any time and break our watchers logic (as it has already happened with the
OVN Load_Balancer table and its datapath field usage).

View File

@ -36,6 +36,7 @@ console_scripts =
ovn_bgp_agent.drivers = ovn_bgp_agent.drivers =
ovn_bgp_driver = ovn_bgp_agent.drivers.openstack.ovn_bgp_driver:OVNBGPDriver ovn_bgp_driver = ovn_bgp_agent.drivers.openstack.ovn_bgp_driver:OVNBGPDriver
nb_ovn_bgp_driver = ovn_bgp_agent.drivers.openstack.nb_ovn_bgp_driver:NBOVNBGPDriver
ovn_evpn_driver = ovn_bgp_agent.drivers.openstack.ovn_evpn_driver:OVNEVPNDriver ovn_evpn_driver = ovn_bgp_agent.drivers.openstack.ovn_evpn_driver:OVNEVPNDriver
ovn_stretched_l2_bgp_driver = ovn_bgp_agent.drivers.openstack.ovn_stretched_l2_bgp_driver:OVNBGPStretchedL2Driver ovn_stretched_l2_bgp_driver = ovn_bgp_agent.drivers.openstack.ovn_stretched_l2_bgp_driver:OVNBGPStretchedL2Driver