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