From cb978dc244dac9de318309606969c0b827b544fe Mon Sep 17 00:00:00 2001 From: Luis Tomas Bolivar Date: Thu, 10 Nov 2022 07:55:00 +0100 Subject: [PATCH] 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 --- ovn_bgp_agent/config.py | 19 +- ovn_bgp_agent/constants.py | 6 + .../drivers/openstack/nb_ovn_bgp_driver.py | 460 ++++++++++++++ .../drivers/openstack/ovn_bgp_driver.py | 6 +- .../drivers/openstack/ovn_evpn_driver.py | 2 +- .../openstack/ovn_stretched_l2_bgp_driver.py | 2 +- ovn_bgp_agent/drivers/openstack/utils/ovn.py | 79 ++- ovn_bgp_agent/drivers/openstack/utils/ovs.py | 10 +- .../openstack/watchers/base_watcher.py | 12 + .../openstack/watchers/nb_bgp_watcher.py | 223 +++++++ .../openstack/test_nb_ovn_bgp_driver.py | 564 ++++++++++++++++++ .../unit/drivers/openstack/utils/test_ovn.py | 110 ++++ .../unit/drivers/openstack/utils/test_ovs.py | 4 +- .../openstack/watchers/test_nb_bgp_watcher.py | 406 +++++++++++++ .../notes/nb_driver-cc7098183fcedb0a.yaml | 9 + setup.cfg | 1 + 16 files changed, 1902 insertions(+), 11 deletions(-) create mode 100644 ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py create mode 100644 ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py create mode 100644 ovn_bgp_agent/tests/unit/drivers/openstack/test_nb_ovn_bgp_driver.py create mode 100644 ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_nb_bgp_watcher.py create mode 100644 releasenotes/notes/nb_driver-cc7098183fcedb0a.yaml diff --git a/ovn_bgp_agent/config.py b/ovn_bgp_agent/config.py index 27942ad5..f944ae6b 100644 --- a/ovn_bgp_agent/config.py +++ b/ovn_bgp_agent/config.py @@ -45,18 +45,33 @@ agent_opts = [ help='The connection string for the native OVSDB backend.\n' 'Use tcp:IP:PORT for TCP connection.\n' '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', - 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 ' 'OVN-SB-DB'), 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 ' 'private key specified in ovn_sb_private_key'), cfg.StrOpt('ovn_sb_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('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', default='64999', help='AS number to be used by the Agent when running in BGP ' diff --git a/ovn_bgp_agent/constants.py b/ovn_bgp_agent/constants.py index 86841301..0264ff6d 100644 --- a/ovn_bgp_agent/constants.py +++ b/ovn_bgp_agent/constants.py @@ -19,9 +19,13 @@ OVN_VM_VIF_PORT_TYPE = "" OVN_PATCH_VIF_PORT_TYPE = "patch" OVN_CHASSISREDIRECT_VIF_PORT_TYPE = "chassisredirect" OVN_LOCALNET_VIF_PORT_TYPE = "localnet" +OVN_DNAT_AND_SNAT = "dnat_and_snat" OVN_CIDRS_EXT_ID_KEY = 'neutron:cidrs' 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-" OVS_RULE_COOKIE = "999" @@ -58,3 +62,5 @@ SUBNET_POOL_ADDR_SCOPE6 = "neutron:subnet_pool_addr_scope6" EXPOSE = "expose" WITHDRAW = "withdraw" + +OVN_REQUESTED_CHASSIS = "requested-chassis" diff --git a/ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py b/ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py new file mode 100644 index 00000000..f2eaed36 --- /dev/null +++ b/ovn_bgp_agent/drivers/openstack/nb_ovn_bgp_driver.py @@ -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 diff --git a/ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py b/ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py index ebd14e3d..9f0e55a7 100644 --- a/ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py +++ b/ovn_bgp_agent/drivers/openstack/ovn_bgp_driver.py @@ -73,7 +73,7 @@ class OVNBGPDriver(driver_api.AgentDriverBase): def start(self): self.ovs_idl = ovs.OvsIdl() 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() 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 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: - VM IP on the provider network, - VM FIP, or @@ -639,7 +639,7 @@ class OVNBGPDriver(driver_api.AgentDriverBase): 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 + This method assumes a device named self.ovn_device exists (inside a VRF), and removes the IP of either: - VM IP on the provider network, - VM FIP, or diff --git a/ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py b/ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py index 3a367574..58981d47 100644 --- a/ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py +++ b/ovn_bgp_agent/drivers/openstack/ovn_evpn_driver.py @@ -67,7 +67,7 @@ class OVNEVPNDriver(driver_api.AgentDriverBase): def start(self): self.ovs_idl = ovs.OvsIdl() 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() LOG.debug("Loaded chassis %s.", self.chassis) diff --git a/ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py b/ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py index 07d9150d..57326e67 100644 --- a/ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py +++ b/ovn_bgp_agent/drivers/openstack/ovn_stretched_l2_bgp_driver.py @@ -88,7 +88,7 @@ class OVNBGPStretchedL2Driver(driver_api.AgentDriverBase): if CONF.clear_vrf_routes_on_startup: 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() LOG.debug("Loaded chassis %s.", self.chassis) if self.allowed_address_scopes: diff --git a/ovn_bgp_agent/drivers/openstack/utils/ovn.py b/ovn_bgp_agent/drivers/openstack/utils/ovn.py index aa8b097c..00431529 100644 --- a/ovn_bgp_agent/drivers/openstack/utils/ovn.py +++ b/ovn_bgp_agent/drivers/openstack/utils/ovn.py @@ -20,6 +20,7 @@ from ovsdbapp.backend import ovs_idl from ovsdbapp.backend.ovs_idl import connection from ovsdbapp.backend.ovs_idl import idlutils 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 ovn_bgp_agent import constants @@ -45,6 +46,47 @@ class OvnDbNotifyHandler(event.RowEventHandler): 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): SCHEMA = 'OVN_Southbound' @@ -85,7 +127,7 @@ class OvnSbIdl(OvnIdl): def start(self): conn = connection.Connection( - self, timeout=180) + self, timeout=CONF.ovsdb_connection_timeout) ovsdbSbConn = OvsdbSbOvnIdl(conn) if self._events: self.notify_handler.watch_events(self._events) @@ -109,6 +151,41 @@ class Backend(ovs_idl.Backend): 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): def __init__(self, connection): super(OvsdbSbOvnIdl, self).__init__(connection) diff --git a/ovn_bgp_agent/drivers/openstack/utils/ovs.py b/ovn_bgp_agent/drivers/openstack/utils/ovs.py index b438d467..d2505c0d 100644 --- a/ovn_bgp_agent/drivers/openstack/utils/ovs.py +++ b/ovn_bgp_agent/drivers/openstack/utils/ovs.py @@ -231,7 +231,7 @@ class OvsIdl(object): return self.idl_ovs.db_get( '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. 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') + 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): """Return the external_ids:ovn-remote value of the Open_vSwitch table. diff --git a/ovn_bgp_agent/drivers/openstack/watchers/base_watcher.py b/ovn_bgp_agent/drivers/openstack/watchers/base_watcher.py index 118192af..2c6e20be 100644 --- a/ovn_bgp_agent/drivers/openstack/watchers/base_watcher.py +++ b/ovn_bgp_agent/drivers/openstack/watchers/base_watcher.py @@ -34,3 +34,15 @@ class OVNLBMemberEvent(row_event.RowEvent): super(OVNLBMemberEvent, self).__init__( events, table, None) 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 diff --git a/ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py b/ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py new file mode 100644 index 00000000..c2297dab --- /dev/null +++ b/ovn_bgp_agent/drivers/openstack/watchers/nb_bgp_watcher.py @@ -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() diff --git a/ovn_bgp_agent/tests/unit/drivers/openstack/test_nb_ovn_bgp_driver.py b/ovn_bgp_agent/tests/unit/drivers/openstack/test_nb_ovn_bgp_driver.py new file mode 100644 index 00000000..b1fe30f3 --- /dev/null +++ b/ovn_bgp_agent/tests/unit/drivers/openstack/test_nb_ovn_bgp_driver.py @@ -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() diff --git a/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovn.py b/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovn.py index b3f0d8c4..9717a3f1 100644 --- a/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovn.py +++ b/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovn.py @@ -30,6 +30,79 @@ from ovn_bgp_agent.tests.unit import fakes 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): def setUp(self): @@ -561,6 +634,43 @@ class TestOvsdbSbOvnIdl(test_base.TestCase): 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): def setUp(self): diff --git a/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovs.py b/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovs.py index a1a34445..72921ca2 100644 --- a/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovs.py +++ b/ovn_bgp_agent/tests/unit/drivers/openstack/utils/test_ovs.py @@ -405,11 +405,11 @@ class TestOvsIdl(test_base.TestCase): self.ovs_idl.idl_ovs.db_get.assert_called_once_with( 'Open_vSwitch', '.', 'external_ids') - def test_get_own_chassis_name(self): + def test_get_own_chassis_id(self): expected_return = 'fake-sys' row = {'system-id': expected_return} 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): expected_return = 'fake-ovn-remote' diff --git a/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_nb_bgp_watcher.py b/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_nb_bgp_watcher.py new file mode 100644 index 00000000..f3683c5d --- /dev/null +++ b/ovn_bgp_agent/tests/unit/drivers/openstack/watchers/test_nb_bgp_watcher.py @@ -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() diff --git a/releasenotes/notes/nb_driver-cc7098183fcedb0a.yaml b/releasenotes/notes/nb_driver-cc7098183fcedb0a.yaml new file mode 100644 index 00000000..4f790a4d --- /dev/null +++ b/releasenotes/notes/nb_driver-cc7098183fcedb0a.yaml @@ -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). diff --git a/setup.cfg b/setup.cfg index bc4e226f..c4ea0af1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,6 +36,7 @@ console_scripts = ovn_bgp_agent.drivers = 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_stretched_l2_bgp_driver = ovn_bgp_agent.drivers.openstack.ovn_stretched_l2_bgp_driver:OVNBGPStretchedL2Driver