From 15260b7439d6b60530f8e728674f07470bc16c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20J=C3=B3zefczyk?= Date: Tue, 17 Dec 2019 12:37:48 +0000 Subject: [PATCH] Add support for multiple L4 protocols withing same LB This change introduces a way of providing multiple listeners and pools on the same Load Balancer, but with different L4 protocols. In case there are more than one unique protocol defined in Octavia pools or Octavia listeners the OVN driver now adds another Load_Balancer row per protocol, but with the same name. The selection of which OVN Load_Balancer should be used is done by a protocol key. This change also adds possibility to expose two listeners on the same port, but with different L4 protocol. The common use-case is providing a DNS service that listens both on UDP and TCP sockets. Partial-Bug: #1789157 Change-Id: I2ee13b75713e7538d1b4f70207a90a95315eb914 --- ovn_octavia_provider/driver.py | 750 +++++++++++------- .../tests/functional/test_driver.py | 365 ++++++--- .../tests/unit/test_driver.py | 435 ++++++++-- ...r-multiple-protocols-4a93e184b8f374c7.yaml | 5 + 4 files changed, 1125 insertions(+), 430 deletions(-) create mode 100644 releasenotes/notes/ovn-octavia-provider-driver-multiple-protocols-4a93e184b8f374c7.yaml diff --git a/ovn_octavia_provider/driver.py b/ovn_octavia_provider/driver.py index edeb706e..7e2304fa 100644 --- a/ovn_octavia_provider/driver.py +++ b/ovn_octavia_provider/driver.py @@ -237,6 +237,12 @@ class OvnProviderHelper(object): REQ_TYPE_HANDLE_VIP_FIP: self.handle_vip_fip, } + @staticmethod + def _is_lb_empty(external_ids): + """Check if there is no pool or listener defined.""" + return not any([k.startswith('listener') or k.startswith('pool') + for k in external_ids]) + def _check_and_set_ssl_files(self): # TODO(reedip): Make ovsdb_monitor's _check_and_set_ssl_files() public # This is a copy of ovsdb_monitor._check_and_set_ssl_files @@ -304,6 +310,25 @@ class OvnProviderHelper(object): ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY)) return router, network + def _clean_lb_if_empty(self, ovn_lb, lb_id, external_ids): + commands = [] + if OvnProviderHelper._is_lb_empty(external_ids): + # Verify if its only OVN LB defined. If so - leave with + # undefined protocol. If there is different for other protocol + # remove this one. + try: + defined_ovn_lbs = self._find_ovn_lbs(lb_id) + except idlutils.RowNotFound: + defined_ovn_lbs = [] + if len(defined_ovn_lbs) == 1: + commands.append( + self.ovn_nbdb_api.db_set( + 'Load_Balancer', ovn_lb.uuid, ('protocol', []))) + else: + # Delete the lb. + commands.append(self.ovn_nbdb_api.lb_del(ovn_lb.uuid)) + return commands + def lb_delete_lrp_assoc_handler(self, row): try: router, network = self._get_nw_router_info_on_interface_event(row) @@ -382,25 +407,26 @@ class OvnProviderHelper(object): port_name = vip_lp.external_ids.get(ovn_const.OVN_PORT_NAME_EXT_ID_KEY) lb_id = port_name[len(ovn_const.LB_VIP_PORT_PREFIX):] try: - ovn_lb = self._find_ovn_lb(lb_id) + ovn_lbs = self._find_ovn_lbs(lb_id) except idlutils.RowNotFound: LOG.debug("Loadbalancer %s not found!", lb_id) return - fip = vip_lp.external_ids.get(ovn_const.OVN_PORT_FIP_EXT_ID_KEY) - lb_vip_fip = ovn_lb.external_ids.get(LB_EXT_IDS_VIP_FIP_KEY) - request_info = {'lb_id': lb_id, - 'vip_fip': fip} - - if fip and fip != lb_vip_fip: - request_info['action'] = REQ_INFO_ACTION_ASSOCIATE - elif fip is None and fip != lb_vip_fip: - request_info['action'] = REQ_INFO_ACTION_DISASSOCIATE - else: - return - - self.add_request({'type': REQ_TYPE_HANDLE_VIP_FIP, - 'info': request_info}) + # Loop over all defined LBs with given ID, because it is possible + # than there is more than one (for more than 1 L4 protocol). + for lb in ovn_lbs: + fip = vip_lp.external_ids.get(ovn_const.OVN_PORT_FIP_EXT_ID_KEY) + lb_vip_fip = lb.external_ids.get(LB_EXT_IDS_VIP_FIP_KEY) + request_info = {'ovn_lb': lb, + 'vip_fip': fip} + if fip and fip != lb_vip_fip: + request_info['action'] = REQ_INFO_ACTION_ASSOCIATE + elif fip is None and fip != lb_vip_fip: + request_info['action'] = REQ_INFO_ACTION_DISASSOCIATE + else: + continue + self.add_request({'type': REQ_TYPE_HANDLE_VIP_FIP, + 'info': request_info}) def _find_lb_in_ls(self, network): """Find LB associated to a Network using Network information @@ -454,18 +480,94 @@ class OvnProviderHelper(object): LOG.error(msg) raise driver_exceptions.UpdateStatusError(msg) - def _find_ovn_lb(self, lb_id): - """Find the Loadbalancer in OVN with the given lb_id as its name + def _find_ovn_lbs(self, lb_id, protocol=None): + """Find the Loadbalancers in OVN with the given lb_id as its name - This function searches for the LoadBalancer whose Name has the pattern + This function searches for the LoadBalancers whose Name has the pattern passed in lb_id. - Input: String format of LoadBalancer ID provided by Octavia in its API + @param lb_id: LoadBalancer ID provided by Octavia in its API request. Note that OVN saves the above ID in the 'name' column. - Output: LoadBalancer row matching the lb_id - Exception: RowNotFound can be generated if the LoadBalancer is not - found. + @type lb_id: str + @param protocol: Loadbalancer protocol. + @type protocol: str or None if not defined. + + :returns: LoadBalancer row if protocol specified + or list of rows matching the lb_id. + :raises: RowNotFound can be generated if the LoadBalancer is not + found. """ - return self.ovn_nbdb_api.lookup('Load_Balancer', lb_id) + lbs = self.ovn_nbdb_api.db_find_rows( + 'Load_Balancer', ('name', '=', lb_id)).execute() + if not protocol: + if lbs: + return lbs + raise idlutils.RowNotFound(table='Load_Balancer', + col='name', match=lb_id) + # If there is only one LB without protocol defined, so + # it is 'clean' LB record without any listener. + if len(lbs) == 1 and not lbs[0].protocol: + return lbs[0] + # Search for other lbs. + for lb in lbs: + if lb.protocol[0].upper() == protocol.upper(): + return lb + raise idlutils.RowNotFound(table='Load_Balancer', + col='name', match=lb_id) + + def _get_or_create_ovn_lb(self, lb_id, protocol, admin_state_up): + """Find or create ovn lb with given protocol + + Find the loadbalancer configured with given protocol or + create required if not found + """ + # Make sure that its lowercase - OVN NBDB stores lowercases + # for this field. + protocol = protocol.lower() + ovn_lbs = self._find_ovn_lbs(lb_id) + lbs_with_required_protocol = [ + ovn_lb for ovn_lb in ovn_lbs + if protocol in ovn_lb.protocol] + lbs_with_no_protocol = [ovn_lb for ovn_lb in ovn_lbs + if not ovn_lb.protocol] + if lbs_with_required_protocol: + # We found existing LB with required + # protocol, just return it. + return lbs_with_required_protocol[0] + elif lbs_with_no_protocol: + ovn_lb = lbs_with_no_protocol[0] + # Set required protocol here. + self.ovn_nbdb_api.db_set( + 'Load_Balancer', ovn_lb.uuid, + ('protocol', protocol)).execute(check_error=True) + else: + # NOTE(mjozefcz): Looks like loadbalancer with given protocol + # doesn't exist. Try to add it with required protocol + # by copy the existing one data. + lb_info = { + 'id': lb_id, + 'protocol': protocol, + 'vip_address': ovn_lbs[0].external_ids.get( + LB_EXT_IDS_VIP_KEY), + 'vip_port_id': + ovn_lbs[0].external_ids.get( + LB_EXT_IDS_VIP_PORT_ID_KEY), + LB_EXT_IDS_LR_REF_KEY: + ovn_lbs[0].external_ids.get( + LB_EXT_IDS_LR_REF_KEY), + LB_EXT_IDS_LS_REFS_KEY: + ovn_lbs[0].external_ids.get( + LB_EXT_IDS_LS_REFS_KEY), + 'admin_state_up': admin_state_up} + # NOTE(mjozefcz): Handle vip_fip info if exists. + vip_fip = ovn_lbs[0].external_ids.get(LB_EXT_IDS_VIP_FIP_KEY) + if vip_fip: + lb_info.update({LB_EXT_IDS_VIP_FIP_KEY: vip_fip}) + self.lb_create(lb_info, protocol=protocol) + # Looks like we've just added new LB + # or updated exising, empty one. + return self._find_ovn_lbs( + lb_id, + protocol=protocol) def _find_ovn_lb_with_pool_key(self, pool_key): lbs = self.ovn_nbdb_api.db_list_rows( @@ -474,7 +576,7 @@ class OvnProviderHelper(object): if pool_key in lb.external_ids: return lb - def _find_ovn_lb_by_id(self, pool_id): + def _find_ovn_lb_by_pool_id(self, pool_id): pool_key = self._get_pool_key(pool_id) ovn_lb = self._find_ovn_lb_with_pool_key(pool_key) if not ovn_lb: @@ -591,13 +693,13 @@ class OvnProviderHelper(object): commands.append(self.ovn_nbdb_api.ls_lb_add( net, ovn_lb.uuid, may_exist=True)) - # Multiple routers in lr_rf are separated with ',' - lr_rf = {LB_EXT_IDS_LR_REF_KEY: ovn_lr.name} if not lr_rf else { - LB_EXT_IDS_LR_REF_KEY: "%s,%s" % (lr_rf, ovn_lr.name)} - commands.append( - self.ovn_nbdb_api.db_set('Load_Balancer', ovn_lb.uuid, - ('external_ids', lr_rf)) - ) + if ovn_lr.name not in str(lr_rf): + # Multiple routers in lr_rf are separated with ',' + lr_rf = {LB_EXT_IDS_LR_REF_KEY: ovn_lr.name} if not lr_rf else { + LB_EXT_IDS_LR_REF_KEY: "%s,%s" % (lr_rf, ovn_lr.name)} + commands.append( + self.ovn_nbdb_api.db_set('Load_Balancer', ovn_lb.uuid, + ('external_ids', lr_rf))) return commands def _update_lb_to_lr_association(self, ovn_lb, ovn_lr, delete=False): @@ -738,7 +840,7 @@ class OvnProviderHelper(object): return False def check_lb_protocol(self, lb_id, listener_protocol): - ovn_lb = self._find_ovn_lb(lb_id) + ovn_lb = self._find_ovn_lbs(lb_id, protocol=listener_protocol) if not ovn_lb: return False elif not self._is_listener_in_lb(ovn_lb): @@ -746,13 +848,15 @@ class OvnProviderHelper(object): else: return str(listener_protocol).lower() in ovn_lb.protocol - def lb_create(self, loadbalancer): + def lb_create(self, loadbalancer, protocol=None): port = None - try: - # Get the port id of the vip and store it in the external_ids. - # This is required to delete the port when the loadbalancer is - # deleted. - network_driver = get_network_driver() + network_driver = get_network_driver() + if loadbalancer.get('vip_port_id'): + # In case we don't have vip_network_id + port = network_driver.neutron_client.show_port( + loadbalancer['vip_port_id'])['port'] + elif (loadbalancer.get('vip_network_id') and + loadbalancer.get('vip_address')): ports = network_driver.neutron_client.list_ports( network_id=loadbalancer['vip_network_id']) for p in ports['ports']: @@ -760,25 +864,63 @@ class OvnProviderHelper(object): if ip['ip_address'] == loadbalancer['vip_address']: port = p break - # In case port is not found for the vip_address we will see an - # exception when port['id'] is accessed. + + # If protocol set make sure its lowercase + protocol = protocol.lower() if protocol else None + # In case port is not found for the vip_address we will see an + # exception when port['id'] is accessed. + external_ids = { + LB_EXT_IDS_VIP_KEY: loadbalancer['vip_address'], + LB_EXT_IDS_VIP_PORT_ID_KEY: + loadbalancer.get('vip_port_id') or port['id'], + 'enabled': str(loadbalancer['admin_state_up'])} + # In case vip_fip was passed - use it. + vip_fip = loadbalancer.get(LB_EXT_IDS_VIP_FIP_KEY) + if vip_fip: + external_ids[LB_EXT_IDS_VIP_FIP_KEY] = vip_fip + # In case of lr_ref passed - use it. + lr_ref = loadbalancer.get(LB_EXT_IDS_LR_REF_KEY) + if lr_ref: + external_ids[LB_EXT_IDS_LR_REF_KEY] = lr_ref + + try: self.ovn_nbdb_api.db_create( - 'Load_Balancer', name=loadbalancer['id'], protocol='tcp', - external_ids={LB_EXT_IDS_VIP_KEY: loadbalancer['vip_address'], - LB_EXT_IDS_VIP_PORT_ID_KEY: port['id'], - 'enabled': str(loadbalancer['admin_state_up'])} - ).execute(check_error=True) - ovn_lb = self._find_ovn_lb(loadbalancer['id']) + 'Load_Balancer', name=loadbalancer['id'], + protocol=protocol, + external_ids=external_ids).execute(check_error=True) + ovn_lb = self._find_ovn_lbs( + loadbalancer['id'], + protocol=protocol) + ovn_lb = ovn_lb if protocol else ovn_lb[0] commands = self._update_lb_to_ls_association( - ovn_lb, network_id=loadbalancer['vip_network_id'], + ovn_lb, network_id=port['network_id'], associate=True) - ls_name = utils.ovn_name(loadbalancer['vip_network_id']) + ls_name = utils.ovn_name(port['network_id']) ovn_ls = self.ovn_nbdb_api.ls_get(ls_name).execute( check_error=True) ovn_lr = self._find_lr_of_ls(ovn_ls) if ovn_lr: commands.extend(self._update_lb_to_lr_association( ovn_lb, ovn_lr)) + + # NOTE(mjozefcz): In case of LS references where passed - + # apply LS to the new LB. That could happend in case we + # need another loadbalancer for other L4 protocol. + ls_refs = loadbalancer.get(LB_EXT_IDS_LS_REFS_KEY) + if ls_refs: + try: + ls_refs = jsonutils.loads(ls_refs) + except ValueError: + ls_refs = {} + for ls in ls_refs: + # Skip previously added LS because we don't want + # to duplicate. + if ls == ovn_ls.name: + continue + commands.extend(self._update_lb_to_ls_association( + ovn_lb, network_id=ls.replace('neutron-', ''), + associate=True)) + self._execute_commands(commands) operating_status = constants.ONLINE # The issue is that since OVN doesnt support any HMs, @@ -809,101 +951,104 @@ class OvnProviderHelper(object): return status def lb_delete(self, loadbalancer): - commands = [] port_id = None + status = {'loadbalancers': [{"id": loadbalancer['id'], + "provisioning_status": "DELETED", + "operating_status": "OFFLINE"}], + 'listeners': [], + 'pools': [], + 'members': []} + + ovn_lbs = None try: - status = {'loadbalancers': [{"id": loadbalancer['id'], - "provisioning_status": "DELETED", - "operating_status": "OFFLINE"}]} - ovn_lb = None - try: - ovn_lb = self._find_ovn_lb(loadbalancer['id']) - except idlutils.RowNotFound: - LOG.warning("Loadbalancer %s not found in OVN Northbound DB." - "Setting the Loadbalancer status to DELETED " - "in Octavia", str(loadbalancer['id'])) - return status - if not ovn_lb: - return status + ovn_lbs = self._find_ovn_lbs(loadbalancer['id']) + except idlutils.RowNotFound: + LOG.warning("Loadbalancer %s not found in OVN Northbound DB. " + "Setting the Loadbalancer status to DELETED " + "in Octavia", str(loadbalancer['id'])) + return status - if loadbalancer['cascade']: - status['members'] = [] - status['pools'] = [] - status['listeners'] = [] - # Delete all pools - for key, value in ovn_lb.external_ids.items(): - if key.startswith(LB_EXT_IDS_POOL_PREFIX): - pool_id = key.split('_')[1] - # Delete all members in the pool - if value and len(value.split(',')) > 0: - for mem_info in value.split(','): - status['members'].append({ - 'id': mem_info.split('_')[1], - 'provisioning_status': constants.DELETED}) - status['pools'].append( - {"id": pool_id, - "provisioning_status": constants.DELETED}) - - if key.startswith(LB_EXT_IDS_LISTENER_PREFIX): - status['listeners'].append({ - 'id': key.split('_')[1], - 'provisioning_status': constants.DELETED, - 'operating_status': constants.OFFLINE}) - # Clear the status dict of any key having [] value - # Python 3.6 doesnt allow deleting an element in a - # dict while iterating over it. So first get a list of keys. - # https://cito.github.io/blog/never-iterate-a-changing-dict/ - status = {key: value for key, value in status.items() if value} - ls_refs = ovn_lb.external_ids.get(LB_EXT_IDS_LS_REFS_KEY, {}) - if ls_refs: - try: - ls_refs = jsonutils.loads(ls_refs) - except ValueError: - ls_refs = {} - # Delete the VIP Port - self.delete_vip_port(ovn_lb.external_ids[ - LB_EXT_IDS_VIP_PORT_ID_KEY]) - for ls_name in ls_refs.keys(): - ovn_ls = self.ovn_nbdb_api.ls_get(ls_name).execute( - check_error=True) - if ovn_ls: - commands.append( - self.ovn_nbdb_api.ls_lb_del(ovn_ls.uuid, ovn_lb.uuid)) - # Delete LB from all Networks the LB is indirectly associated - for ls in self._find_lb_in_table(ovn_lb, 'Logical_Switch'): - commands.append( - self.ovn_nbdb_api.ls_lb_del(ls.uuid, ovn_lb.uuid, - if_exists=True)) - lr_ref = ovn_lb.external_ids.get(LB_EXT_IDS_LR_REF_KEY, {}) - if lr_ref: - for lr in self.ovn_nbdb_api.tables[ - 'Logical_Router'].rows.values(): - if lr.name == lr_ref: - commands.append(self.ovn_nbdb_api.lr_lb_del( - lr.uuid, ovn_lb.uuid)) - break - # Delete LB from all Routers the LB is indirectly associated - for lr in self._find_lb_in_table(ovn_lb, 'Logical_Router'): - commands.append( - self.ovn_nbdb_api.lr_lb_del(lr.uuid, ovn_lb.uuid, - if_exists=True)) - # Save the port ID before deleting the LoadBalancer - port_id = ovn_lb.external_ids[LB_EXT_IDS_VIP_PORT_ID_KEY] - commands.append(self.ovn_nbdb_api.lb_del(ovn_lb.uuid)) - self._execute_commands(commands) - # We need to delete the vip port but not fail LB delete if port - # delete fails. Can happen when Port deleted manually by user. - network_driver = get_network_driver() - network_driver.neutron_client.delete_port(port_id) - except n_exc.PortNotFoundClient: - LOG.warning("Port %s could not be found. Please " - "check Neutron logs", port_id) + try: + port_id = ovn_lbs[0].external_ids[LB_EXT_IDS_VIP_PORT_ID_KEY] + for ovn_lb in ovn_lbs: + status = self._lb_delete(loadbalancer, ovn_lb, status) + # Clear the status dict of any key having [] value + # Python 3.6 doesnt allow deleting an element in a + # dict while iterating over it. So first get a list of keys. + # https://cito.github.io/blog/never-iterate-a-changing-dict/ + status = {key: value for key, value in status.items() if value} except Exception: LOG.exception(EXCEPTION_MSG, "deletion of loadbalancer") status = { 'loadbalancers': [{"id": loadbalancer['id'], "provisioning_status": constants.ERROR, "operating_status": constants.ERROR}]} + try: + # Delete VIP port from neutron. + self.delete_vip_port(port_id) + except n_exc.PortNotFoundClient: + LOG.warning("Port %s could not be found. Please " + "check Neutron logs", port_id) + return status + + def _lb_delete(self, loadbalancer, ovn_lb, status): + commands = [] + if loadbalancer['cascade']: + # Delete all pools + for key, value in ovn_lb.external_ids.items(): + if key.startswith(LB_EXT_IDS_POOL_PREFIX): + pool_id = key.split('_')[1] + # Delete all members in the pool + if value and len(value.split(',')) > 0: + for mem_info in value.split(','): + status['members'].append({ + 'id': mem_info.split('_')[1], + 'provisioning_status': constants.DELETED}) + status['pools'].append( + {"id": pool_id, + "provisioning_status": constants.DELETED}) + + if key.startswith(LB_EXT_IDS_LISTENER_PREFIX): + status['listeners'].append({ + 'id': key.split('_')[1], + 'provisioning_status': constants.DELETED, + 'operating_status': constants.OFFLINE}) + ls_refs = ovn_lb.external_ids.get(LB_EXT_IDS_LS_REFS_KEY, {}) + if ls_refs: + try: + ls_refs = jsonutils.loads(ls_refs) + except ValueError: + ls_refs = {} + for ls_name in ls_refs.keys(): + try: + ovn_ls = self.ovn_nbdb_api.ls_get(ls_name).execute( + check_error=True) + commands.append( + self.ovn_nbdb_api.ls_lb_del(ovn_ls.uuid, ovn_lb.uuid) + ) + except idlutils.RowNotFound: + LOG.warning("LogicalSwitch %s could not be found. Cannot " + "delete LB from it", ovn_ls.uuid) + # Delete LB from all Networks the LB is indirectly associated + for ls in self._find_lb_in_table(ovn_lb, 'Logical_Switch'): + commands.append( + self.ovn_nbdb_api.ls_lb_del(ls.uuid, ovn_lb.uuid, + if_exists=True)) + lr_ref = ovn_lb.external_ids.get(LB_EXT_IDS_LR_REF_KEY, {}) + if lr_ref: + for lr in self.ovn_nbdb_api.tables[ + 'Logical_Router'].rows.values(): + if lr.name == lr_ref: + commands.append(self.ovn_nbdb_api.lr_lb_del( + lr.uuid, ovn_lb.uuid)) + break + # Delete LB from all Routers the LB is indirectly associated + for lr in self._find_lb_in_table(ovn_lb, 'Logical_Router'): + commands.append( + self.ovn_nbdb_api.lr_lb_del(lr.uuid, ovn_lb.uuid, + if_exists=True)) + commands.append(self.ovn_nbdb_api.lb_del(ovn_lb.uuid)) + self._execute_commands(commands) return status def lb_failover(self, loadbalancer): @@ -913,30 +1058,36 @@ class OvnProviderHelper(object): return status def lb_update(self, loadbalancer): - try: - lb_status = {'id': loadbalancer['id'], - 'provisioning_status': constants.ACTIVE} - status = {'loadbalancers': [lb_status]} - if 'admin_state_up' not in loadbalancer: - return status - lb_enabled = loadbalancer['admin_state_up'] + lb_status = {'id': loadbalancer['id'], + 'provisioning_status': constants.ACTIVE} + status = {'loadbalancers': [lb_status]} + if 'admin_state_up' not in loadbalancer: + return status + lb_enabled = loadbalancer['admin_state_up'] - ovn_lb = self._find_ovn_lb(loadbalancer['id']) - if ovn_lb.external_ids['enabled'] != str(lb_enabled): - commands = [] - enable_info = {'enabled': str(lb_enabled)} - ovn_lb.external_ids['enabled'] = str(lb_enabled) - commands.append( - self.ovn_nbdb_api.db_set('Load_Balancer', ovn_lb.uuid, - ('external_ids', enable_info))) - commands.extend( - self._refresh_lb_vips(ovn_lb.uuid, ovn_lb.external_ids)) - self._execute_commands(commands) - if lb_enabled: - operating_status = constants.ONLINE - else: - operating_status = constants.OFFLINE - lb_status['operating_status'] = operating_status + try: + ovn_lbs = self._find_ovn_lbs(loadbalancer['id']) + # It should be unique for all the LBS for all protocols, + # so we could just easly loop over all defined for given + # Octavia LB. + for ovn_lb in ovn_lbs: + if str(ovn_lb.external_ids['enabled']) != str(lb_enabled): + commands = [] + enable_info = {'enabled': str(lb_enabled)} + ovn_lb.external_ids['enabled'] = str(lb_enabled) + commands.append( + self.ovn_nbdb_api.db_set('Load_Balancer', ovn_lb.uuid, + ('external_ids', enable_info)) + ) + commands.extend( + self._refresh_lb_vips(ovn_lb.uuid, + ovn_lb.external_ids)) + self._execute_commands(commands) + if lb_enabled: + operating_status = constants.ONLINE + else: + operating_status = constants.OFFLINE + lb_status['operating_status'] = operating_status except Exception: LOG.exception(EXCEPTION_MSG, "update of loadbalancer") lb_status['provisioning_status'] = constants.ERROR @@ -944,21 +1095,24 @@ class OvnProviderHelper(object): return status def listener_create(self, listener): + ovn_lb = self._get_or_create_ovn_lb( + listener['loadbalancer_id'], + listener['protocol'], + listener['admin_state_up']) + + external_ids = copy.deepcopy(ovn_lb.external_ids) + listener_key = self._get_listener_key( + listener['id'], is_enabled=listener['admin_state_up']) + + if listener.get('default_pool_id'): + pool_key = self._get_pool_key(listener['default_pool_id']) + else: + pool_key = '' + external_ids[listener_key] = self._make_listener_key_value( + listener['protocol_port'], pool_key) + + listener_info = {listener_key: external_ids[listener_key]} try: - ovn_lb = self._find_ovn_lb(listener['loadbalancer_id']) - - external_ids = copy.deepcopy(ovn_lb.external_ids) - listener_key = self._get_listener_key( - listener['id'], is_enabled=listener['admin_state_up']) - - if listener.get('default_pool_id'): - pool_key = self._get_pool_key(listener['default_pool_id']) - else: - pool_key = '' - external_ids[listener_key] = self._make_listener_key_value( - listener['protocol_port'], pool_key) - - listener_info = {listener_key: external_ids[listener_key]} commands = [] commands.append( self.ovn_nbdb_api.db_set('Load_Balancer', ovn_lb.uuid, @@ -995,73 +1149,95 @@ class OvnProviderHelper(object): return status def listener_delete(self, listener): + status = { + 'listeners': [{"id": listener['id'], + "provisioning_status": constants.DELETED, + "operating_status": constants.OFFLINE}], + 'loadbalancers': [{"id": listener['loadbalancer_id'], + "provisioning_status": constants.ACTIVE}]} try: - ovn_lb = self._find_ovn_lb(listener['loadbalancer_id']) - external_ids = copy.deepcopy(ovn_lb.external_ids) - listener_key = self._get_listener_key(listener['id']) + ovn_lb = self._find_ovn_lbs( + listener['loadbalancer_id'], + protocol=listener['protocol']) + except idlutils.RowNotFound: + # Listener already deleted. + return status - if listener_key in external_ids: + external_ids = copy.deepcopy(ovn_lb.external_ids) + listener_key = self._get_listener_key(listener['id']) + if listener_key in external_ids: + try: commands = [] commands.append( self.ovn_nbdb_api.db_remove( 'Load_Balancer', ovn_lb.uuid, 'external_ids', - (listener_key)) - ) - + (listener_key))) + # Drop current listener from LB. del external_ids[listener_key] + + # Set LB protocol to undefined only if there are no more + # listeners and pools defined in the LB. commands.extend( - self._refresh_lb_vips(ovn_lb.uuid, external_ids) - ) - + self._clean_lb_if_empty( + ovn_lb, listener['loadbalancer_id'], external_ids)) + commands.extend( + self._refresh_lb_vips(ovn_lb.uuid, external_ids)) self._execute_commands(commands) - - status = { - 'listeners': [{"id": listener['id'], - "provisioning_status": constants.DELETED, - "operating_status": constants.OFFLINE}], - 'loadbalancers': [{"id": listener['loadbalancer_id'], - "provisioning_status": constants.ACTIVE}]} - except Exception: - LOG.exception(EXCEPTION_MSG, "deletion of listener") - status = { - 'listeners': [{"id": listener['id'], - "provisioning_status": constants.ERROR, - "operating_status": constants.ERROR}], - 'loadbalancers': [{"id": listener['loadbalancer_id'], - "provisioning_status": constants.ACTIVE}]} + except Exception: + LOG.exception(EXCEPTION_MSG, "deletion of listener") + status = { + 'listeners': [{ + "id": listener['id'], + "provisioning_status": constants.ERROR, + "operating_status": constants.ERROR}], + 'loadbalancers': [{ + "id": listener['loadbalancer_id'], + "provisioning_status": constants.ACTIVE}]} return status def listener_update(self, listener): + # NOTE(mjozefcz): Based on + # https://docs.openstack.org/api-ref/load-balancer/v2/?expanded=update-a-listener-detail + # there is no possibility to update listener protocol or port. + listener_status = {'id': listener['id'], + 'provisioning_status': constants.ACTIVE} + pool_status = [] + status = { + 'listeners': [listener_status], + 'loadbalancers': [{'id': listener['loadbalancer_id'], + 'provisioning_status': constants.ACTIVE}], + 'pools': pool_status} + try: - listener_status = {'id': listener['id'], - 'provisioning_status': constants.ACTIVE} - pool_status = [] - status = { - 'listeners': [listener_status], - 'loadbalancers': [{'id': listener['loadbalancer_id'], - 'provisioning_status': constants.ACTIVE}], - 'pools': pool_status} + ovn_lb = self._find_ovn_lbs( + listener['loadbalancer_id'], + protocol=listener['protocol']) + except idlutils.RowNotFound: + LOG.exception(EXCEPTION_MSG, "update of listener") + # LB row not found during updating a listner. That is a problem. + status['listeners'][0]['provisioning_status'] = constants.ERROR + status['loadbalancers'][0]['provisioning_status'] = constants.ERROR + return status - ovn_lb = self._find_ovn_lb(listener['loadbalancer_id']) - l_key_when_enabled = self._get_listener_key(listener['id']) - l_key_when_disabled = self._get_listener_key( - listener['id'], is_enabled=False) + l_key_when_enabled = self._get_listener_key(listener['id']) + l_key_when_disabled = self._get_listener_key( + listener['id'], is_enabled=False) - external_ids = copy.deepcopy(ovn_lb.external_ids) + external_ids = copy.deepcopy(ovn_lb.external_ids) + if 'admin_state_up' not in listener and ( + 'default_pool_id' not in listener): + return status - if 'admin_state_up' not in listener and ( - 'default_pool_id' not in listener): - return status - - l_key_to_add = {} - if l_key_when_enabled in external_ids: - present_l_key = l_key_when_enabled - elif l_key_when_disabled in external_ids: - present_l_key = l_key_when_disabled - else: - # Something is terribly wrong. This cannot happen. - return status + l_key_to_add = {} + if l_key_when_enabled in external_ids: + present_l_key = l_key_when_enabled + elif l_key_when_disabled in external_ids: + present_l_key = l_key_when_disabled + else: + # Something is terribly wrong. This cannot happen. + return status + try: commands = [] new_l_key = None l_key_to_remove = None @@ -1116,21 +1292,23 @@ class OvnProviderHelper(object): 'provisioning_status': constants.ERROR}], 'loadbalancers': [{'id': listener['loadbalancer_id'], 'provisioning_status': constants.ACTIVE}]} - return status def pool_create(self, pool): + ovn_lb = self._get_or_create_ovn_lb( + pool['loadbalancer_id'], + pool['protocol'], + pool['admin_state_up']) + external_ids = copy.deepcopy(ovn_lb.external_ids) + pool_key = self._get_pool_key(pool['id'], + is_enabled=pool['admin_state_up']) + external_ids[pool_key] = '' + if pool['listener_id']: + listener_key = self._get_listener_key(pool['listener_id']) + if listener_key in ovn_lb.external_ids: + external_ids[listener_key] = str( + external_ids[listener_key]) + str(pool_key) try: - ovn_lb = self._find_ovn_lb(pool['loadbalancer_id']) - external_ids = copy.deepcopy(ovn_lb.external_ids) - pool_key = self._get_pool_key(pool['id'], - is_enabled=pool['admin_state_up']) - external_ids[pool_key] = '' - if pool['listener_id']: - listener_key = self._get_listener_key(pool['listener_id']) - if listener_key in ovn_lb.external_ids: - external_ids[listener_key] = str( - external_ids[listener_key]) + str(pool_key) self.ovn_nbdb_api.db_set( 'Load_Balancer', ovn_lb.uuid, ('external_ids', external_ids)).execute(check_error=True) @@ -1162,11 +1340,23 @@ class OvnProviderHelper(object): return status def pool_delete(self, pool): + status = { + 'pools': [{"id": pool['id'], + "provisioning_status": constants.DELETED}], + 'loadbalancers': [{"id": pool['loadbalancer_id'], + "provisioning_status": constants.ACTIVE}]} + try: + ovn_lb = self._find_ovn_lbs( + pool['loadbalancer_id'], + pool['protocol']) + except idlutils.RowNotFound: + # LB row not found that means pool is deleted. + return status + + pool_key = self._get_pool_key(pool['id']) + commands = [] + external_ids = copy.deepcopy(ovn_lb.external_ids) try: - ovn_lb = self._find_ovn_lb(pool['loadbalancer_id']) - pool_key = self._get_pool_key(pool['id']) - commands = [] - external_ids = copy.deepcopy(ovn_lb.external_ids) if pool_key in ovn_lb.external_ids: commands.append( self.ovn_nbdb_api.db_remove('Load_Balancer', ovn_lb.uuid, @@ -1196,13 +1386,11 @@ class OvnProviderHelper(object): 'external_ids', (pool_key_when_disabled)) ) + commands.extend( + self._clean_lb_if_empty( + ovn_lb, pool['loadbalancer_id'], external_ids)) self._execute_commands(commands) - status = { - 'pools': [{"id": pool['id'], - "provisioning_status": constants.DELETED}], - 'loadbalancers': [{"id": pool['loadbalancer_id'], - "provisioning_status": constants.ACTIVE}]} if listener_id: status['listeners'] = [{ 'id': listener_id, @@ -1218,24 +1406,32 @@ class OvnProviderHelper(object): return status def pool_update(self, pool): + pool_status = {'id': pool['id'], + 'provisioning_status': constants.ACTIVE} + status = { + 'pools': [pool_status], + 'loadbalancers': [{'id': pool['loadbalancer_id'], + 'provisioning_status': constants.ACTIVE}]} + if 'admin_state_up' not in pool: + return status try: - pool_status = {'id': pool['id'], - 'provisioning_status': constants.ACTIVE} - status = { - 'pools': [pool_status], - 'loadbalancers': [{'id': pool['loadbalancer_id'], - 'provisioning_status': constants.ACTIVE}]} - if 'admin_state_up' not in pool: - return status + ovn_lb = self._find_ovn_lbs( + pool['loadbalancer_id'], protocol=pool['protocol']) + except idlutils.RowNotFound: + LOG.exception(EXCEPTION_MSG, "update of pool") + # LB row not found during updating a listner. That is a problem. + status['pool'][0]['provisioning_status'] = constants.ERROR + status['loadbalancers'][0]['provisioning_status'] = constants.ERROR + return status - ovn_lb = self._find_ovn_lb(pool['loadbalancer_id']) - pool_key = self._get_pool_key(pool['id']) - p_key_when_disabled = self._get_pool_key(pool['id'], - is_enabled=False) + pool_key = self._get_pool_key(pool['id']) + p_key_when_disabled = self._get_pool_key(pool['id'], + is_enabled=False) + external_ids = copy.deepcopy(ovn_lb.external_ids) + p_key_to_remove = None + p_key_to_add = {} - external_ids = copy.deepcopy(ovn_lb.external_ids) - p_key_to_remove = None - p_key_to_add = {} + try: if pool['admin_state_up']: if p_key_when_disabled in external_ids: p_key_to_add[pool_key] = external_ids[p_key_when_disabled] @@ -1320,7 +1516,7 @@ class OvnProviderHelper(object): def member_create(self, member): try: - pool_key, ovn_lb = self._find_ovn_lb_by_id( + pool_key, ovn_lb = self._find_ovn_lb_by_pool_id( member['pool_id']) self._add_member(member, ovn_lb, pool_key) pool = {"id": member['pool_id'], @@ -1386,7 +1582,7 @@ class OvnProviderHelper(object): def member_delete(self, member): try: - pool_key, ovn_lb = self._find_ovn_lb_by_id( + pool_key, ovn_lb = self._find_ovn_lb_by_pool_id( member['pool_id']) pool_status = self._remove_member(member, ovn_lb, pool_key) pool = {"id": member['pool_id'], @@ -1440,7 +1636,7 @@ class OvnProviderHelper(object): def member_update(self, member): try: - pool_key, ovn_lb = self._find_ovn_lb_by_id( + pool_key, ovn_lb = self._find_ovn_lb_by_pool_id( member['pool_id']) status = { 'pools': [{'id': member['pool_id'], @@ -1476,7 +1672,7 @@ class OvnProviderHelper(object): return status def _get_existing_pool_members(self, pool_id): - pool_key, ovn_lb = self._find_ovn_lb_by_id(pool_id) + pool_key, ovn_lb = self._find_ovn_lb_by_pool_id(pool_id) if not ovn_lb: msg = _("Loadbalancer with pool %s does not exist") % pool_key raise driver_exceptions.DriverError(msg) @@ -1535,11 +1731,7 @@ class OvnProviderHelper(object): network_driver.neutron_client.delete_port(port_id) def handle_vip_fip(self, fip_info): - try: - ovn_lb = self._find_ovn_lb(fip_info['lb_id']) - except idlutils.RowNotFound: - LOG.debug("Loadbalancer %s not found!", fip_info['lb_id']) - return + ovn_lb = fip_info['ovn_lb'] external_ids = copy.deepcopy(ovn_lb.external_ids) commands = [] @@ -1627,6 +1819,7 @@ class OvnProviderDriver(driver_base.ProviderDriver): admin_state_up = True request_info = {'id': pool.pool_id, 'loadbalancer_id': pool.loadbalancer_id, + 'protocol': pool.protocol, 'listener_id': pool.listener_id, 'admin_state_up': admin_state_up} request = {'type': REQ_TYPE_POOL_CREATE, @@ -1638,6 +1831,7 @@ class OvnProviderDriver(driver_base.ProviderDriver): self.member_delete(member) request_info = {'id': pool.pool_id, + 'protocol': pool.protocol, 'loadbalancer_id': pool.loadbalancer_id} request = {'type': REQ_TYPE_POOL_DELETE, 'info': request_info} @@ -1648,7 +1842,8 @@ class OvnProviderDriver(driver_base.ProviderDriver): self._check_for_supported_protocols(new_pool.protocol) if not isinstance(new_pool.lb_algorithm, o_datamodels.UnsetType): self._check_for_supported_algorithms(new_pool.lb_algorithm) - request_info = {'id': new_pool.pool_id, + request_info = {'id': old_pool.pool_id, + 'protocol': old_pool.protocol, 'loadbalancer_id': old_pool.loadbalancer_id} if not isinstance(new_pool.admin_state_up, o_datamodels.UnsetType): @@ -1657,21 +1852,8 @@ class OvnProviderDriver(driver_base.ProviderDriver): 'info': request_info} self._ovn_helper.add_request(request) - # Listener - def _check_listener_protocol(self, listener): - self._check_for_supported_protocols(listener.protocol) - if not self._ovn_helper.check_lb_protocol( - listener.loadbalancer_id, listener.protocol): - msg = (_('The loadbalancer %(lb)s does not support %(proto)s ' - 'protocol') % { - 'lb': listener.loadbalancer_id, - 'proto': listener.protocol}) - raise driver_exceptions.UnsupportedOptionError( - user_fault_string=msg, - operator_fault_string=msg) - def listener_create(self, listener): - self._check_listener_protocol(listener) + self._check_for_supported_protocols(listener.protocol) admin_state_up = listener.admin_state_up if isinstance(admin_state_up, o_datamodels.UnsetType): admin_state_up = True @@ -1688,16 +1870,16 @@ class OvnProviderDriver(driver_base.ProviderDriver): def listener_delete(self, listener): request_info = {'id': listener.listener_id, 'loadbalancer_id': listener.loadbalancer_id, - 'protocol_port': listener.protocol_port} + 'protocol_port': listener.protocol_port, + 'protocol': listener.protocol} request = {'type': REQ_TYPE_LISTENER_DELETE, 'info': request_info} self._ovn_helper.add_request(request) def listener_update(self, old_listener, new_listener): - if not isinstance(new_listener.protocol, o_datamodels.UnsetType): - self._check_listener_protocol(new_listener) request_info = {'id': new_listener.listener_id, 'loadbalancer_id': old_listener.loadbalancer_id, + 'protocol': old_listener.protocol, 'protocol_port': old_listener.protocol_port} if not isinstance(new_listener.admin_state_up, o_datamodels.UnsetType): @@ -1721,7 +1903,7 @@ class OvnProviderDriver(driver_base.ProviderDriver): return False def _ip_version_differs(self, member): - _, ovn_lb = self._ovn_helper._find_ovn_lb_by_id(member.pool_id) + _, ovn_lb = self._ovn_helper._find_ovn_lb_by_pool_id(member.pool_id) lb_vip = ovn_lb.external_ids[LB_EXT_IDS_VIP_KEY] return netaddr.IPNetwork(lb_vip).version != ( netaddr.IPNetwork(member.address).version) diff --git a/ovn_octavia_provider/tests/functional/test_driver.py b/ovn_octavia_provider/tests/functional/test_driver.py index d31849e2..d05e0640 100644 --- a/ovn_octavia_provider/tests/functional/test_driver.py +++ b/ovn_octavia_provider/tests/functional/test_driver.py @@ -13,8 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -import mock +import copy +import mock from neutron.common import utils as n_utils from neutron_lib.plugins import directory from octavia_lib.api.drivers import data_models as octavia_data_model @@ -65,6 +66,8 @@ class TestOctaviaOvnProviderDriver( self.fake_network_driver.get_subnet = self._mock_get_subnet self.fake_network_driver.neutron_client.list_ports = ( self._mock_list_ports) + self.fake_network_driver.neutron_client.show_port = ( + self._mock_show_port) self.fake_network_driver.neutron_client.\ delete_port.return_value = True self._local_net_cache = {} @@ -80,6 +83,11 @@ class TestOctaviaOvnProviderDriver( def _mock_list_ports(self, **kwargs): return self._local_port_cache + def _mock_show_port(self, port_id): + for port in self._local_port_cache['ports']: + if port['id'] == port_id: + return {'port': port} + def _create_provider_network(self): e1 = self._make_network(self.fmt, 'e1', True, arg_list=('router:external', @@ -129,7 +137,6 @@ class TestOctaviaOvnProviderDriver( m_pool.loadbalancer_id = loadbalancer_id m_pool.members = [] m_pool.admin_state_up = admin_state_up - m_pool.protocol = protocol m_pool.lb_algorithm = lb_algorithm if listener_id: m_pool.listener_id = listener_id @@ -189,7 +196,26 @@ class TestOctaviaOvnProviderDriver( def _validate_loadbalancers(self, expected_lbs): observed_lbs = self._get_loadbalancers() - self.assertItemsEqual(expected_lbs, observed_lbs) + # NOTE (mjozefcz): assertItemsEqual works only on first level + # of comparison, if dicts inside dics are in diffrent + # order it would fail. + self.assertEqual(len(expected_lbs), + len(observed_lbs)) + for expected_lb in expected_lbs: + # search for LB with same name and protocol + found = False + for observed_lb in observed_lbs: + if (observed_lb.get('name') == + expected_lb.get('name') and + observed_lb.get('protocol') == + expected_lb.get('protocol')): + self.assertEqual(expected_lb, observed_lb) + found = True + if not found: + raise Exception("Expected LB %(name)s for protocol %(proto)s " + "not found in observed_lbs", { + 'name': expected_lb.get('name'), + 'proto': expected_lb.get('proto')}) def _is_lb_associated_to_ls(self, lb_name, ls_name): return self._is_lb_associated_to_tab( @@ -238,30 +264,54 @@ class TestOctaviaOvnProviderDriver( net_id = LR_REF_KEY_HEADER + '%s' % net_id if add_ref: - if net_id in lb_data[ovn_driver.LB_EXT_IDS_LS_REFS_KEY]: - ref_ct = lb_data[ - ovn_driver.LB_EXT_IDS_LS_REFS_KEY][net_id] + 1 - lb_data[ovn_driver.LB_EXT_IDS_LS_REFS_KEY][net_id] = ref_ct - else: + if net_id not in lb_data[ovn_driver.LB_EXT_IDS_LS_REFS_KEY]: lb_data[ovn_driver.LB_EXT_IDS_LS_REFS_KEY][net_id] = 1 else: - ref_ct = lb_data[ovn_driver.LB_EXT_IDS_LS_REFS_KEY][net_id] - 1 - if ref_ct > 0: - lb_data[ovn_driver.LB_EXT_IDS_LS_REFS_KEY][net_id] = ref_ct - else: + ref_ct = lb_data[ovn_driver.LB_EXT_IDS_LS_REFS_KEY][net_id] + if ref_ct <= 0: del lb_data[ovn_driver.LB_EXT_IDS_LS_REFS_KEY][net_id] - def _wait_for_status(self, expected_status, check_call=True): - call_count = len(expected_status) - expected_calls = [mock.call(status) for status in expected_status] + def _wait_for_status(self, expected_statuses, check_call=True): + call_count = len(expected_statuses) update_loadbalancer_status = ( self._o_driver_lib.update_loadbalancer_status) n_utils.wait_until_true( lambda: update_loadbalancer_status.call_count == call_count, timeout=10) if check_call: - self._o_driver_lib.update_loadbalancer_status.assert_has_calls( - expected_calls, any_order=True) + # NOTE(mjozefcz): The updates are send in parallel and includes + # dicts with unordered lists inside. So we can't simply use + # assert_has_calls here. Sample structure: + # {'listeners': [], + # 'loadbalancers': [{'id': 'a', 'provisioning_status': 'ACTIVE'}], + # 'members': [{'id': 'b', 'provisioning_status': 'DELETED'}, + # {'id': 'c', 'provisioning_status': 'DELETED'}], + # 'pools': [{'id': 'd', 'operating_status': 'ONLINE', + # 'provisioning_status': 'ACTIVE'}]}, + updated_statuses = [] + for call in update_loadbalancer_status.mock_calls: + updated_statuses.append(call.args[0]) + calls_found = [] + for expected_status in expected_statuses: + for updated_status in updated_statuses: + # Find status update having equal keys + if updated_status.keys() == expected_status.keys(): + val_check = [] + # Withing this status update check if all values of + # expected keys match. + for k, v in expected_status.items(): + val_check.append( + sorted(expected_status[k], + key=lambda x: x['id']) == + sorted(updated_status[k], + key=lambda x: x['id'])) + if False in val_check: + # At least one value don't match. + continue + calls_found.append(expected_status) + break + # Validate if we found all expected calls. + self.assertItemsEqual(expected_statuses, calls_found) def _wait_for_status_and_validate(self, lb_data, expected_status, check_call=True): @@ -399,9 +449,55 @@ class TestOctaviaOvnProviderDriver( 'neutron:vip': lb_data['model'].vip_address, 'neutron:vip_port_id': vip_net_info[3], 'enabled': str(lb_data['model'].admin_state_up)} + # NOTE(mjozefcz): By default we don't set protocol. We don't know if + # listener/pool would be TCP or UDP, so do not set it. + expected_protocols = set() + + # Lets fetch list of L4 protocols defined for this LB. + for p in lb_data['pools']: + expected_protocols.add(p.protocol.lower()) + for l in lb_data['listeners']: + expected_protocols.add(l.protocol.lower()) + # If there is no protocol lets add default - empty []. + expected_protocols = list(expected_protocols) + if len(expected_protocols) == 0: + expected_protocols.append(None) + + expected_lbs = [{'name': lb_data['model'].loadbalancer_id, + 'protocol': [protocol] if protocol else [], + 'vips': {}, + 'external_ids': copy.deepcopy(external_ids)} + for protocol in expected_protocols] + + def _get_lb_field_by_protocol(protocol, field='external_ids'): + "Get needed external_ids and pass by reference" + lb = [lb for lb in expected_lbs + if lb.get('protocol') == [protocol]] + return lb[0].get(field) + + # For every connected subnet to the LB set the ref + # counter. + for net_id, ref_ct in lb_data[ + ovn_driver.LB_EXT_IDS_LS_REFS_KEY].items(): + for lb in expected_lbs: + # If given LB hasn't VIP configured from + # this network we shouldn't touch it here. + if net_id == 'neutron-%s' % lb_data['model'].vip_network_id: + lb.get('external_ids')[ + ovn_driver.LB_EXT_IDS_LS_REFS_KEY][net_id] = 1 + + # For every connected router set it here. + if lb_data.get(ovn_driver.LB_EXT_IDS_LR_REF_KEY): + for lb in expected_lbs: + lb.get('external_ids')[ + ovn_driver.LB_EXT_IDS_LR_REF_KEY] = lb_data[ + ovn_driver.LB_EXT_IDS_LR_REF_KEY] pool_info = {} for p in lb_data.get('pools', []): + external_ids = _get_lb_field_by_protocol( + p.protocol.lower(), + field='external_ids') p_members = "" for m in p.members: if not m.admin_state_up: @@ -412,23 +508,31 @@ class TestOctaviaOvnProviderDriver( p_members += "," + m_info else: p_members = m_info + # Bump up LS refs counter if needed. + if m.subnet_id: + # Need to get the network_id. + for port in self._local_port_cache['ports']: + for fixed_ip in port['fixed_ips']: + if fixed_ip['subnet_id'] == m.subnet_id: + ex = external_ids[ + ovn_driver.LB_EXT_IDS_LS_REFS_KEY] + act = ex.get( + 'neutron-%s' % port['network_id'], 0) + ex['neutron-%s' % port['network_id']] = act + 1 + break pool_key = 'pool_' + p.pool_id if not p.admin_state_up: pool_key += ':D' external_ids[pool_key] = p_members pool_info[p.pool_id] = p_members - for net_id, ref_ct in lb_data[ - ovn_driver.LB_EXT_IDS_LS_REFS_KEY].items(): - external_ids[ovn_driver.LB_EXT_IDS_LS_REFS_KEY][net_id] = ref_ct - - if lb_data.get(ovn_driver.LB_EXT_IDS_LR_REF_KEY): - external_ids[ - ovn_driver.LB_EXT_IDS_LR_REF_KEY] = lb_data[ - ovn_driver.LB_EXT_IDS_LR_REF_KEY] - expected_vips = {} - expected_protocol = ['tcp'] for l in lb_data['listeners']: + expected_vips = _get_lb_field_by_protocol( + l.protocol.lower(), + field='vips') + external_ids = _get_lb_field_by_protocol( + l.protocol.lower(), + field='external_ids') listener_k = 'listener_' + str(l.listener_id) if lb_data['model'].admin_state_up and l.admin_state_up: vip_k = lb_data['model'].vip_address + ":" + str( @@ -447,11 +551,6 @@ class TestOctaviaOvnProviderDriver( elif lb_data.get('pools', []): external_ids[listener_k] += 'pool_' + lb_data[ 'pools'][0].pool_id - - expected_lbs = [{'name': lb_data['model'].loadbalancer_id, - 'protocol': expected_protocol, - 'vips': expected_vips, - 'external_ids': external_ids}] return expected_lbs def _extract_member_info(self, member): @@ -462,10 +561,12 @@ class TestOctaviaOvnProviderDriver( return mem_info[:-1] def _create_pool_and_validate(self, lb_data, pool_name, + protocol=None, listener_id=None): lb_pools = lb_data['pools'] m_pool = self._create_pool_model(lb_data['model'].loadbalancer_id, pool_name, + protocol=protocol, listener_id=listener_id) lb_pools.append(m_pool) self._o_driver_lib.update_loadbalancer_status.reset_mock() @@ -567,9 +668,10 @@ class TestOctaviaOvnProviderDriver( if pool_name and p.name == pool_name: return p - def _get_listener_from_lb_data(self, lb_data, protocol_port): + def _get_listener_from_lb_data(self, lb_data, protocol, protocol_port): for l in lb_data['listeners']: - if l.protocol_port == protocol_port: + if (l.protocol_port == protocol_port and + l.protocol == protocol): return l def _get_pool_listeners(self, lb_data, pool_id): @@ -721,9 +823,10 @@ class TestOctaviaOvnProviderDriver( self._wait_for_status_and_validate(lb_data, [expected_status]) - def _update_listener_and_validate(self, lb_data, protocol_port, + def _update_listener_and_validate(self, lb_data, protocol_port=80, admin_state_up=None, protocol='TCP'): - m_listener = self._get_listener_from_lb_data(lb_data, protocol_port) + m_listener = self._get_listener_from_lb_data( + lb_data, protocol, protocol_port) self._o_driver_lib.update_loadbalancer_status.reset_mock() old_admin_state_up = m_listener.admin_state_up operating_status = 'ONLINE' @@ -752,8 +855,10 @@ class TestOctaviaOvnProviderDriver( self._wait_for_status_and_validate(lb_data, [expected_status]) - def _delete_listener_and_validate(self, lb_data, protocol_port=80): - m_listener = self._get_listener_from_lb_data(lb_data, protocol_port) + def _delete_listener_and_validate(self, lb_data, protocol='TCP', + protocol_port=80): + m_listener = self._get_listener_from_lb_data( + lb_data, protocol, protocol_port) lb_data['listeners'].remove(m_listener) self._o_driver_lib.update_loadbalancer_status.reset_mock() self.ovn_driver.listener_delete(m_listener) @@ -789,7 +894,10 @@ class TestOctaviaOvnProviderDriver( expected_status = { 'loadbalancers': [{"id": lb_data['model'].loadbalancer_id, "provisioning_status": "DELETED", - "operating_status": "OFFLINE"}] + "operating_status": "OFFLINE"}], + 'listeners': [], + 'pools': [], + 'members': [], } del lb_data['model'] self._wait_for_status_and_validate(lb_data, [expected_status]) @@ -798,12 +906,21 @@ class TestOctaviaOvnProviderDriver( lb_data = self._create_load_balancer_and_validate( {'vip_network': 'vip_network', 'cidr': '10.0.0.0/24'}) - self._create_pool_and_validate(lb_data, "p1") + self._create_pool_and_validate(lb_data, "p1", + protocol='TCP') self._update_pool_and_validate(lb_data, "p1") - self._update_pool_and_validate(lb_data, "p1", admin_state_up=True) - self._update_pool_and_validate(lb_data, "p1", admin_state_up=False) - self._update_pool_and_validate(lb_data, "p1", admin_state_up=True) - self._create_pool_and_validate(lb_data, "p2") + self._create_pool_and_validate(lb_data, "p2", + protocol='UDP') + self._create_pool_and_validate(lb_data, "p3", + protocol='TCP') + self._update_pool_and_validate(lb_data, "p3", + admin_state_up=False) + self._update_pool_and_validate(lb_data, "p3", + admin_state_up=True) + self._update_pool_and_validate(lb_data, "p3", + admin_state_up=False) + self._create_pool_and_validate(lb_data, "p4", + protocol='UDP') self._delete_pool_and_validate(lb_data, "p2") self._delete_pool_and_validate(lb_data, "p1") self._delete_load_balancer_and_validate(lb_data) @@ -812,50 +929,72 @@ class TestOctaviaOvnProviderDriver( lb_data = self._create_load_balancer_and_validate( {'vip_network': 'vip_network', 'cidr': '10.0.0.0/24'}) - self._create_pool_and_validate(lb_data, "p1") - pool_id = lb_data['pools'][0].pool_id - self._create_member_and_validate( - lb_data, pool_id, lb_data['vip_net_info'][1], - lb_data['vip_net_info'][0], '10.0.0.10') - self._update_member_and_validate(lb_data, pool_id, "10.0.0.10") + # TCP Pool + self._create_pool_and_validate(lb_data, "p1", + protocol='TCP') + + # UDP Pool + self._create_pool_and_validate(lb_data, "p2", + protocol='UDP') + + pool_1_id = lb_data['pools'][0].pool_id + pool_2_id = lb_data['pools'][1].pool_id + + # Members for TCP Pool self._create_member_and_validate( - lb_data, pool_id, lb_data['vip_net_info'][1], + lb_data, pool_1_id, lb_data['vip_net_info'][1], + lb_data['vip_net_info'][0], '10.0.0.10') + self._update_member_and_validate(lb_data, pool_1_id, "10.0.0.10") + self._create_member_and_validate( + lb_data, pool_1_id, lb_data['vip_net_info'][1], + lb_data['vip_net_info'][0], '10.0.0.11') + + # Members for UDP Pool + self._create_member_and_validate( + lb_data, pool_2_id, lb_data['vip_net_info'][1], + lb_data['vip_net_info'][0], '10.0.0.10') + self._update_member_and_validate(lb_data, pool_1_id, "10.0.0.10") + self._create_member_and_validate( + lb_data, pool_2_id, lb_data['vip_net_info'][1], lb_data['vip_net_info'][0], '10.0.0.11') # Disable loadbalancer self._update_load_balancer_and_validate(lb_data, admin_state_up=False) - # Enable loadbalancer back self._update_load_balancer_and_validate(lb_data, admin_state_up=True) - self._delete_member_and_validate(lb_data, pool_id, + + # Delete members from TCP Pool + self._delete_member_and_validate(lb_data, pool_1_id, lb_data['vip_net_info'][0], '10.0.0.10') - self._delete_member_and_validate(lb_data, pool_id, + self._delete_member_and_validate(lb_data, pool_1_id, lb_data['vip_net_info'][0], '10.0.0.11') + # Add again member to TCP Pool self._create_member_and_validate( - lb_data, pool_id, lb_data['vip_net_info'][1], + lb_data, pool_1_id, lb_data['vip_net_info'][1], lb_data['vip_net_info'][0], '10.0.0.10') + # Create new networks and add member to TCP pool from it. net20_info = self._create_net('net20', '20.0.0.0/24') net20 = net20_info[0] subnet20 = net20_info[1] - self._create_member_and_validate(lb_data, pool_id, subnet20, net20, + self._create_member_and_validate(lb_data, pool_1_id, subnet20, net20, '20.0.0.4') - self._create_member_and_validate(lb_data, pool_id, subnet20, net20, + self._create_member_and_validate(lb_data, pool_1_id, subnet20, net20, '20.0.0.6') net30_info = self._create_net('net30', '30.0.0.0/24') net30 = net30_info[0] subnet30 = net30_info[1] - self._create_member_and_validate(lb_data, pool_id, subnet30, net30, + self._create_member_and_validate(lb_data, pool_1_id, subnet30, net30, '30.0.0.6') - self._delete_member_and_validate(lb_data, pool_id, net20, '20.0.0.6') + self._delete_member_and_validate(lb_data, pool_1_id, net20, '20.0.0.6') # Test creating Member without subnet - m_member = self._create_member_model(pool_id, + m_member = self._create_member_model(pool_1_id, None, '30.0.0.7', 80) self.assertRaises(o_exceptions.UnsupportedOptionError, @@ -863,36 +1002,76 @@ class TestOctaviaOvnProviderDriver( # Deleting the pool should also delete the members. self._delete_pool_and_validate(lb_data, "p1") + + # Delete the whole LB. self._delete_load_balancer_and_validate(lb_data) def test_listener(self): lb_data = self._create_load_balancer_and_validate( {'vip_network': 'vip_network', 'cidr': '10.0.0.0/24'}) - self._create_pool_and_validate(lb_data, "p1") - pool_id = lb_data['pools'][0].pool_id - self._create_member_and_validate( - lb_data, pool_id, lb_data['vip_net_info'][1], - lb_data['vip_net_info'][0], '10.0.0.4') - + self._create_pool_and_validate(lb_data, "p1", + protocol='TCP') + self._create_pool_and_validate(lb_data, "p2", + protocol='UDP') + pool_1_id = lb_data['pools'][0].pool_id + pool_2_id = lb_data['pools'][1].pool_id net_info = self._create_net('net1', '20.0.0.0/24') - self._create_member_and_validate(lb_data, pool_id, - net_info[1], net_info[0], '20.0.0.4') - self._create_listener_and_validate(lb_data, pool_id, 80) - self._update_listener_and_validate(lb_data, 80) - self._update_listener_and_validate(lb_data, 80, admin_state_up=True) - self._update_listener_and_validate(lb_data, 80, admin_state_up=False) - self._update_listener_and_validate(lb_data, 80, admin_state_up=True) - self._create_listener_and_validate(lb_data, pool_id, 82) - self._delete_listener_and_validate(lb_data, 82) - self._delete_listener_and_validate(lb_data, 80) - self._delete_member_and_validate(lb_data, pool_id, + # Create member in first pool + self._create_member_and_validate( + lb_data, pool_1_id, lb_data['vip_net_info'][1], + lb_data['vip_net_info'][0], '10.0.0.4') + self._create_member_and_validate(lb_data, pool_1_id, + net_info[1], net_info[0], '20.0.0.4') + + # Create member in second pool + self._create_member_and_validate( + lb_data, pool_2_id, lb_data['vip_net_info'][1], + lb_data['vip_net_info'][0], '10.0.0.4') + self._create_member_and_validate(lb_data, pool_2_id, + net_info[1], net_info[0], '20.0.0.4') + + # Play around first listener linked to first pool. + self._create_listener_and_validate( + lb_data, pool_1_id, 80, protocol='TCP') + self._update_listener_and_validate(lb_data, protocol_port=80) + self._update_listener_and_validate( + lb_data, protocol_port=80, admin_state_up=True) + self._update_listener_and_validate( + lb_data, protocol_port=80, admin_state_up=False) + self._update_listener_and_validate( + lb_data, protocol_port=80, admin_state_up=True) + self._create_listener_and_validate( + lb_data, pool_1_id, protocol_port=82, protocol='TCP') + + # Play around second listener linked to second pool. + self._create_listener_and_validate( + lb_data, pool_2_id, 53, protocol='UDP') + self._update_listener_and_validate(lb_data, 53, protocol='UDP') + self._update_listener_and_validate( + lb_data, protocol_port=53, protocol='UDP', admin_state_up=True) + self._update_listener_and_validate( + lb_data, protocol_port=53, protocol='UDP', admin_state_up=False) + self._update_listener_and_validate( + lb_data, protocol_port=53, protocol='UDP', admin_state_up=True) + self._create_listener_and_validate( + lb_data, pool_2_id, protocol_port=21, protocol='UDP') + + # Delete listeners linked to first pool. + self._delete_listener_and_validate( + lb_data, protocol_port=82, protocol='TCP') + self._delete_listener_and_validate( + lb_data, protocol_port=80, protocol='TCP') + # Delete first pool members. + self._delete_member_and_validate(lb_data, pool_1_id, net_info[0], '20.0.0.4') - self._delete_member_and_validate(lb_data, pool_id, + self._delete_member_and_validate(lb_data, pool_1_id, lb_data['vip_net_info'][0], '10.0.0.4') + # Delete empty, first pool self._delete_pool_and_validate(lb_data, "p1") + # Delete the rest self._delete_load_balancer_and_validate(lb_data) def _test_cascade_delete(self, pool=True, listener=True, member=True): @@ -900,14 +1079,22 @@ class TestOctaviaOvnProviderDriver( {'vip_network': 'vip_network', 'cidr': '10.0.0.0/24'}) if pool: - self._create_pool_and_validate(lb_data, "p1") - pool_id = lb_data['pools'][0].pool_id + self._create_pool_and_validate(lb_data, "p1", protocol='TCP') + self._create_pool_and_validate(lb_data, "p2", protocol='UDP') + pool_1_id = lb_data['pools'][0].pool_id + pool_2_id = lb_data['pools'][1].pool_id if member: self._create_member_and_validate( - lb_data, pool_id, lb_data['vip_net_info'][1], + lb_data, pool_1_id, lb_data['vip_net_info'][1], + lb_data['vip_net_info'][0], '10.0.0.10') + self._create_member_and_validate( + lb_data, pool_2_id, lb_data['vip_net_info'][1], lb_data['vip_net_info'][0], '10.0.0.10') if listener: - self._create_listener_and_validate(lb_data, pool_id, 80) + self._create_listener_and_validate( + lb_data, pool_1_id, protocol_port=80, protocol='TCP') + self._create_listener_and_validate( + lb_data, pool_2_id, protocol_port=53, protocol='UDP') self._delete_load_balancer_and_validate(lb_data, cascade=True) @@ -937,12 +1124,6 @@ class TestOctaviaOvnProviderDriver( self.assertRaises(o_exceptions.UnsupportedOptionError, self.ovn_driver.listener_create, m_listener) self._create_listener_and_validate(lb_data) - self.assertRaises(o_exceptions.UnsupportedOptionError, - self._create_listener_and_validate, - lb_data, protocol_port=80, protocol='UDP') - self.assertRaises(o_exceptions.UnsupportedOptionError, - self._update_listener_and_validate, - lb_data, protocol_port=80, protocol='UDP') self._delete_load_balancer_and_validate(lb_data) def _test_lrp_event_handler(self, cascade=False): @@ -1065,10 +1246,10 @@ class TestOctaviaOvnProviderDriver( {'vip_network': 'vip_network', 'cidr': '10.0.0.0/24'}) self._create_listener_and_validate(lb_data) - self._create_pool_and_validate(lb_data, "p1", - lb_data['listeners'][0].listener_id) - self._delete_pool_and_validate(lb_data, "p1", - lb_data['listeners'][0].listener_id) + self._create_pool_and_validate( + lb_data, "p1", listener_id=lb_data['listeners'][0].listener_id) + self._delete_pool_and_validate( + lb_data, "p1", listener_id=lb_data['listeners'][0].listener_id) self._delete_listener_and_validate(lb_data) self._delete_load_balancer_and_validate(lb_data) diff --git a/ovn_octavia_provider/tests/unit/test_driver.py b/ovn_octavia_provider/tests/unit/test_driver.py index 80395917..0e70e7a0 100644 --- a/ovn_octavia_provider/tests/unit/test_driver.py +++ b/ovn_octavia_provider/tests/unit/test_driver.py @@ -11,6 +11,7 @@ # License for the specific language governing permissions and limitations # under the License. # +import copy import os import mock @@ -134,6 +135,7 @@ class TestOvnProviderDriver(TestOvnOctaviaBase): add_req_thread = mock.patch.object(ovn_driver.OvnProviderHelper, 'add_request') self.ovn_lb = mock.MagicMock() + self.ovn_lb.name = 'foo_ovn_lb' self.ovn_lb.external_ids = { ovn_driver.LB_EXT_IDS_VIP_KEY: '10.22.33.4'} self.mock_add_request = add_req_thread.start() @@ -262,8 +264,10 @@ class TestOvnProviderDriver(TestOvnOctaviaBase): project_id=self.project_id, vip_address=self.vip_address, vip_network_id=self.vip_network_id) - mock.patch.object(ovn_driver.OvnProviderHelper, '_find_ovn_lb', - return_value=self.ovn_lb).start() + mock.patch.object( + ovn_driver.OvnProviderHelper, '_find_ovn_lbs', + side_effect=lambda x, protocol=None: + self.ovn_lb if protocol else [self.ovn_lb]).start() mock.patch.object( ovn_driver.OvnProviderHelper, 'get_member_info', return_value=[ @@ -438,9 +442,32 @@ class TestOvnProviderDriver(TestOvnOctaviaBase): self.driver.listener_create(self.ref_listener) self.mock_add_request.assert_called_once_with(expected_dict) + def test_listener_create_unsupported_protocol(self): + self.assertRaises(exceptions.UnsupportedOptionError, + self.driver.listener_create, self.fail_listener) + + def test_listener_create_multiple_protocols(self): + self.ovn_lb.protocol = ['tcp'] + info = {'id': self.ref_listener.listener_id, + 'protocol': self.ref_listener.protocol, + 'protocol_port': self.ref_listener.protocol_port, + 'default_pool_id': self.ref_listener.default_pool_id, + 'admin_state_up': self.ref_listener.admin_state_up, + 'loadbalancer_id': self.ref_listener.loadbalancer_id} + expected_dict = {'type': ovn_driver.REQ_TYPE_LISTENER_CREATE, + 'info': info} + self.driver.listener_create(self.ref_listener) + self.mock_add_request.assert_called_once_with(expected_dict) + self.ovn_lb.protocol = ['UDP'] + info['protocol'] = 'UDP' + expected_dict = {'type': ovn_driver.REQ_TYPE_LISTENER_CREATE, + 'info': info} + self.driver.listener_create(self.ref_listener) + def test_listener_update(self): info = {'id': self.ref_listener.listener_id, 'protocol_port': self.ref_listener.protocol_port, + 'protocol': self.ref_pool.protocol, 'admin_state_up': self.ref_listener.admin_state_up, 'loadbalancer_id': self.ref_listener.loadbalancer_id} if self.ref_listener.default_pool_id: @@ -450,22 +477,10 @@ class TestOvnProviderDriver(TestOvnOctaviaBase): self.driver.listener_update(self.ref_listener, self.ref_listener) self.mock_add_request.assert_called_once_with(expected_dict) - @mock.patch.object(ovn_driver.OvnProviderHelper, '_is_listener_in_lb', - return_value=True) - def test_listener_failure(self, mock_listener): - self.assertRaises(exceptions.UnsupportedOptionError, - self.driver.listener_create, self.fail_listener) - self.assertRaises(exceptions.UnsupportedOptionError, - self.driver.listener_update, self.ref_listener, - self.fail_listener) - self.ovn_lb.protocol = ['TCP'] - self.assertRaises(exceptions.UnsupportedOptionError, - self.driver.listener_create, - self.ref_listener_udp) - def test_listener_delete(self): info = {'id': self.ref_listener.listener_id, 'protocol_port': self.ref_listener.protocol_port, + 'protocol': self.ref_pool.protocol, 'loadbalancer_id': self.ref_listener.loadbalancer_id} expected_dict = {'type': ovn_driver.REQ_TYPE_LISTENER_DELETE, 'info': info} @@ -516,7 +531,7 @@ class TestOvnProviderDriver(TestOvnOctaviaBase): self.driver.loadbalancer_failover(info['id']) self.mock_add_request.assert_called_once_with(expected_dict) - def test_pool_create_http(self): + def test_pool_create_unsupported_protocol(self): self.ref_pool.protocol = 'HTTP' self.assertRaises(exceptions.UnsupportedOptionError, self.driver.pool_create, self.ref_pool) @@ -530,6 +545,7 @@ class TestOvnProviderDriver(TestOvnOctaviaBase): info = {'id': self.ref_pool.pool_id, 'loadbalancer_id': self.ref_pool.loadbalancer_id, 'listener_id': self.ref_pool.listener_id, + 'protocol': self.ref_pool.protocol, 'admin_state_up': self.ref_pool.admin_state_up} expected_dict = {'type': ovn_driver.REQ_TYPE_POOL_CREATE, 'info': info} @@ -540,6 +556,7 @@ class TestOvnProviderDriver(TestOvnOctaviaBase): self.ref_pool.admin_state_up = data_models.UnsetType() info = {'id': self.ref_pool.pool_id, 'loadbalancer_id': self.ref_pool.loadbalancer_id, + 'protocol': self.ref_pool.protocol, 'listener_id': self.ref_pool.listener_id, 'admin_state_up': True} expected_dict = {'type': ovn_driver.REQ_TYPE_POOL_CREATE, @@ -551,6 +568,7 @@ class TestOvnProviderDriver(TestOvnOctaviaBase): # Pretent we don't have members self.ref_pool.members = [] info = {'id': self.ref_pool.pool_id, + 'protocol': self.ref_pool.protocol, 'loadbalancer_id': self.ref_pool.loadbalancer_id} expected = {'type': ovn_driver.REQ_TYPE_POOL_DELETE, 'info': info} @@ -559,6 +577,7 @@ class TestOvnProviderDriver(TestOvnOctaviaBase): def test_pool_delete_with_members(self): info = {'id': self.ref_pool.pool_id, + 'protocol': self.ref_pool.protocol, 'loadbalancer_id': self.ref_pool.loadbalancer_id} expected = {'type': ovn_driver.REQ_TYPE_POOL_DELETE, 'info': info} @@ -578,6 +597,7 @@ class TestOvnProviderDriver(TestOvnOctaviaBase): def test_pool_update(self): info = {'id': self.ref_update_pool.pool_id, 'loadbalancer_id': self.ref_update_pool.loadbalancer_id, + 'protocol': self.ref_pool.protocol, 'admin_state_up': self.ref_update_pool.admin_state_up} expected_dict = {'type': ovn_driver.REQ_TYPE_POOL_UPDATE, 'info': info} @@ -629,10 +649,12 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): 'admin_state_up': False} self.ports = {'ports': [{ 'fixed_ips': [{'ip_address': self.vip_address}], + 'network_id': self.vip_network_id, 'id': self.port1_id}]} self.pool = {'id': self.pool_id, 'loadbalancer_id': self.loadbalancer_id, 'listener_id': self.listener_id, + 'protocol': "TCP", 'admin_state_up': False} self.member = {'id': self.member_id, 'address': self.member_address, @@ -646,6 +668,7 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): 'add_request') self.mock_add_request = add_req_thread.start() self.ovn_lb = mock.MagicMock() + self.ovn_lb.protocol = ['tcp'] self.ovn_lb.uuid = uuidutils.generate_uuid() self.member_line = ( 'member_%s_%s:%s' % @@ -664,8 +687,13 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): mock.patch.object(self.helper, '_find_ovn_lb_with_pool_key', return_value=self.ovn_lb).start() - mock.patch.object(ovn_driver.OvnProviderHelper, '_find_ovn_lb', - return_value=self.ovn_lb).start() + + self.mock_find_ovn_lbs = mock.patch.object( + ovn_driver.OvnProviderHelper, '_find_ovn_lbs', + side_effect=lambda x, protocol=None: + self.ovn_lb if protocol else [self.ovn_lb]) + self.mock_find_ovn_lbs.start() + mock.patch.object(self.helper, '_get_pool_listeners', return_value=[]).start() @@ -727,6 +755,114 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): (self.helper.ovn_nbdb_api.ls_get.return_value. execute.return_value) = self.network + def test__is_lb_empty(self): + f = self.helper._is_lb_empty + self.assertFalse(f(self.ovn_lb.external_ids)) + self.ovn_lb.external_ids.pop('listener_%s' % self.listener_id) + self.assertFalse(f(self.ovn_lb.external_ids)) + self.ovn_lb.external_ids.pop('pool_%s' % self.pool_id) + self.assertTrue(f(self.ovn_lb.external_ids)) + + def test__find_ovn_lbs(self): + self.mock_find_ovn_lbs.stop() + f = self.helper._find_ovn_lbs + self.helper.ovn_nbdb_api.db_find_rows.return_value.\ + execute.return_value = [self.ovn_lb] + + # Without protocol specified return a list + found = f(self.ovn_lb.id) + self.assertListEqual(found, [self.ovn_lb]) + self.helper.ovn_nbdb_api.db_find_rows.assert_called_once_with( + 'Load_Balancer', ('name', '=', self.ovn_lb.id)) + self.helper.ovn_nbdb_api.db_find_rows.reset_mock() + + # With protocol specified return an instance + found = f(self.ovn_lb.id, protocol='tcp') + self.assertEqual(found, self.ovn_lb) + self.helper.ovn_nbdb_api.db_find_rows.reset_mock() + + # LB with given protocol not found + self.helper.ovn_nbdb_api.db_find_rows.return_value.\ + execute.return_value = [] + self.assertRaises( + idlutils.RowNotFound, + f, + self.ovn_lb.id, + protocol='UDP') + + # Multiple protocols + udp_lb = copy.copy(self.ovn_lb) + udp_lb.protocol = ['udp'] + self.helper.ovn_nbdb_api.db_find_rows.return_value.\ + execute.return_value = [self.ovn_lb, udp_lb] + found = f(self.ovn_lb.id) + self.assertListEqual(found, [self.ovn_lb, udp_lb]) + + def test__get_or_create_ovn_lb_no_lb_found(self): + self.mock_find_ovn_lbs.stop() + self.helper.ovn_nbdb_api.db_find_rows.return_value.\ + execute.return_value = [] + self.assertRaises( + idlutils.RowNotFound, + self.helper._get_or_create_ovn_lb, + self.ovn_lb.name, + protocol='TCP', + admin_state_up='True') + + @mock.patch.object(ovn_driver.OvnProviderHelper, 'lb_create') + def test__get_or_create_ovn_lb_required_proto_not_found(self, lbc): + udp_lb = copy.copy(self.ovn_lb) + udp_lb.protocol = ['udp'] + self.mock_find_ovn_lbs.stop() + self.helper.ovn_nbdb_api.db_find_rows.return_value.\ + execute.side_effect = [[udp_lb], [self.ovn_lb]] + self.helper._get_or_create_ovn_lb( + self.ovn_lb.name, + protocol='TCP', + admin_state_up='True') + expected_lb_info = { + 'id': self.ovn_lb.name, + 'protocol': 'tcp', + 'vip_address': udp_lb.external_ids.get( + ovn_driver.LB_EXT_IDS_VIP_KEY), + 'vip_port_id': + udp_lb.external_ids.get( + ovn_driver.LB_EXT_IDS_VIP_PORT_ID_KEY), + ovn_driver.LB_EXT_IDS_LR_REF_KEY: + udp_lb.external_ids.get( + ovn_driver.LB_EXT_IDS_LR_REF_KEY), + ovn_driver.LB_EXT_IDS_LS_REFS_KEY: + udp_lb.external_ids.get( + ovn_driver.LB_EXT_IDS_LS_REFS_KEY), + 'admin_state_up': 'True', + ovn_driver.LB_EXT_IDS_VIP_FIP_KEY: + udp_lb.external_ids.get( + ovn_driver.LB_EXT_IDS_VIP_FIP_KEY)} + lbc.assert_called_once_with(expected_lb_info, protocol='tcp') + + def test__get_or_create_ovn_lb_found(self): + self.mock_find_ovn_lbs.stop() + self.helper.ovn_nbdb_api.db_find_rows.return_value.\ + execute.return_value = [self.ovn_lb] + found = self.helper._get_or_create_ovn_lb( + self.ovn_lb.name, + protocol='TCP', + admin_state_up='True') + self.assertEqual(found, self.ovn_lb) + + def test__get_or_create_ovn_lb_lb_without_protocol(self): + self.mock_find_ovn_lbs.stop() + self.ovn_lb.protocol = [] + self.helper.ovn_nbdb_api.db_find_rows.return_value.\ + execute.return_value = [self.ovn_lb] + found = self.helper._get_or_create_ovn_lb( + self.ovn_lb.name, + protocol='TCP', + admin_state_up='True') + self.assertEqual(found, self.ovn_lb) + self.helper.ovn_nbdb_api.db_set.assert_called_once_with( + 'Load_Balancer', self.ovn_lb.uuid, ('protocol', 'tcp')) + @mock.patch('ovn_octavia_provider.driver.get_network_driver') def test_lb_create_disabled(self, net_dr): self.lb['admin_state_up'] = False @@ -743,7 +879,7 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): ovn_driver.LB_EXT_IDS_VIP_PORT_ID_KEY: mock.ANY, 'enabled': 'False'}, name=mock.ANY, - protocol='tcp') + protocol=None) @mock.patch('ovn_octavia_provider.driver.get_network_driver') def test_lb_create_enabled(self, net_dr): @@ -761,12 +897,43 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): ovn_driver.LB_EXT_IDS_VIP_PORT_ID_KEY: mock.ANY, 'enabled': 'True'}, name=mock.ANY, - protocol='tcp') + protocol=None) + + @mock.patch('ovn_octavia_provider.driver.get_network_driver') + def test_lb_create_on_multi_protocol(self, net_dr): + """This test situation when new protocol is added + + to the same loadbalancer and we need to add + additional OVN lb with the same name. + """ + self.lb['admin_state_up'] = True + self.lb['protocol'] = 'UDP' + self.lb[ovn_driver.LB_EXT_IDS_LR_REF_KEY] = 'foo' + self.lb[ovn_driver.LB_EXT_IDS_LS_REFS_KEY] = "{\"neutron-foo\": 1}" + net_dr.return_value.neutron_client.list_ports.return_value = ( + self.ports) + status = self.helper.lb_create(self.lb, protocol='UDP') + self.assertEqual(status['loadbalancers'][0]['provisioning_status'], + constants.ACTIVE) + self.assertEqual(status['loadbalancers'][0]['operating_status'], + constants.ONLINE) + self.helper.ovn_nbdb_api.db_create.assert_called_once_with( + 'Load_Balancer', external_ids={ + ovn_driver.LB_EXT_IDS_VIP_KEY: mock.ANY, + ovn_driver.LB_EXT_IDS_VIP_PORT_ID_KEY: mock.ANY, + ovn_driver.LB_EXT_IDS_LR_REF_KEY: 'foo', + 'enabled': 'True'}, + name=mock.ANY, + protocol='udp') + self.helper._update_lb_to_ls_association.assert_has_calls([ + mock.call(self.ovn_lb, associate=True, + network_id=self.lb['vip_network_id']), + mock.call(self.ovn_lb, associate=True, network_id='foo')]) @mock.patch('ovn_octavia_provider.driver.get_network_driver') @mock.patch.object(ovn_driver.OvnProviderHelper, 'delete_vip_port') def test_lb_create_exception(self, del_port, net_dr): - self.helper._find_ovn_lb.side_effect = [RuntimeError] + self.helper._find_ovn_lbs.side_effect = [RuntimeError] net_dr.return_value.neutron_client.list_ports.return_value = ( self.ports) status = self.helper.lb_create(self.lb) @@ -791,7 +958,7 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): @mock.patch.object(ovn_driver.OvnProviderHelper, 'delete_vip_port') def test_lb_delete_row_not_found(self, del_port): - self.helper._find_ovn_lb.side_effect = [idlutils.RowNotFound] + self.helper._find_ovn_lbs.side_effect = [idlutils.RowNotFound] status = self.helper.lb_delete(self.lb) self.assertEqual(status['loadbalancers'][0]['provisioning_status'], constants.DELETED) @@ -803,13 +970,14 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): @mock.patch('ovn_octavia_provider.driver.get_network_driver') @mock.patch.object(ovn_driver.OvnProviderHelper, 'delete_vip_port') def test_lb_delete_exception(self, del_port, net_dr): - del_port.side_effect = [RuntimeError] + self.helper.ovn_nbdb_api.lb_del.side_effect = [RuntimeError] status = self.helper.lb_delete(self.lb) self.assertEqual(status['loadbalancers'][0]['provisioning_status'], constants.ERROR) self.assertEqual(status['loadbalancers'][0]['operating_status'], constants.ERROR) - self.helper.ovn_nbdb_api.lb_del.assert_not_called() + self.helper.ovn_nbdb_api.lb_del.assert_called_once_with( + self.ovn_lb.uuid) del_port.assert_called_once_with('foo_port') @mock.patch('ovn_octavia_provider.driver.get_network_driver') @@ -817,12 +985,13 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): def test_lb_delete_port_not_found(self, del_port, net_dr): net_dr.return_value.neutron_client.delete_port.return_value = None del_port.side_effect = [n_exc.PortNotFoundClient] - status = self.helper.lb_delete(self.lb) + status = self.helper.lb_delete(self.ovn_lb) self.assertEqual(status['loadbalancers'][0]['provisioning_status'], constants.DELETED) self.assertEqual(status['loadbalancers'][0]['operating_status'], constants.OFFLINE) - self.helper.ovn_nbdb_api.lb_del.assert_not_called() + self.helper.ovn_nbdb_api.lb_del.assert_called_once_with( + self.ovn_lb.uuid) del_port.assert_called_once_with('foo_port') @mock.patch('ovn_octavia_provider.driver.get_network_driver') @@ -860,6 +1029,20 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): self.helper.ovn_nbdb_api.lr_lb_del.assert_called_once_with( self.router.uuid, self.ovn_lb.uuid) + @mock.patch('ovn_octavia_provider.driver.get_network_driver') + def test_lb_delete_multiple_protocols(self, net_dr): + net_dr.return_value.neutron_client.delete_port.return_value = None + self.mock_find_ovn_lbs.stop() + udp_lb = copy.copy(self.ovn_lb) + udp_lb.protocol = ['udp'] + udp_lb.uuid = 'foo_uuid' + self.helper.ovn_nbdb_api.db_find_rows.return_value.\ + execute.return_value = [self.ovn_lb, udp_lb] + self.helper.lb_delete(self.lb) + self.helper.ovn_nbdb_api.lb_del.assert_has_calls([ + mock.call(self.ovn_lb.uuid), + mock.call(udp_lb.uuid)]) + def test_lb_failover(self): status = self.helper.lb_failover(self.lb) self.assertEqual(status['loadbalancers'][0]['provisioning_status'], @@ -867,6 +1050,7 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): @mock.patch.object(ovn_driver.OvnProviderHelper, '_refresh_lb_vips') def test_lb_update_disabled(self, refresh_vips): + self.lb['admin_state_up'] = False status = self.helper.lb_update(self.lb) self.assertEqual(status['loadbalancers'][0]['provisioning_status'], constants.ACTIVE) @@ -880,6 +1064,8 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): @mock.patch.object(ovn_driver.OvnProviderHelper, '_refresh_lb_vips') def test_lb_update_enabled(self, refresh_vips): + # Change the mock, its enabled by default. + self.ovn_lb.external_ids.update({'enabled': False}) self.lb['admin_state_up'] = True status = self.helper.lb_update(self.lb) self.assertEqual(status['loadbalancers'][0]['provisioning_status'], @@ -892,8 +1078,35 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): 'Load_Balancer', self.ovn_lb.uuid, ('external_ids', {'enabled': 'True'})) + @mock.patch.object(ovn_driver.OvnProviderHelper, '_refresh_lb_vips') + def test_lb_update_enabled_multiple_protocols(self, refresh_vips): + self.mock_find_ovn_lbs.stop() + self.ovn_lb.external_ids.update({'enabled': 'False'}) + udp_lb = copy.deepcopy(self.ovn_lb) + udp_lb.protocol = ['udp'] + udp_lb.uuid = 'foo_uuid' + self.helper.ovn_nbdb_api.db_find_rows.return_value.\ + execute.return_value = [self.ovn_lb, udp_lb] + self.lb['admin_state_up'] = True + status = self.helper.lb_update(self.lb) + self.assertEqual(status['loadbalancers'][0]['provisioning_status'], + constants.ACTIVE) + self.assertEqual(status['loadbalancers'][0]['operating_status'], + constants.ONLINE) + refresh_vips.assert_has_calls([ + mock.call(self.ovn_lb.uuid, self.ovn_lb.external_ids), + mock.ANY, + mock.ANY, + mock.call(udp_lb.uuid, udp_lb.external_ids)], + any_order=False) + self.helper.ovn_nbdb_api.db_set.assert_has_calls([ + mock.call('Load_Balancer', self.ovn_lb.uuid, + ('external_ids', {'enabled': 'True'})), + mock.call('Load_Balancer', udp_lb.uuid, + ('external_ids', {'enabled': 'True'}))]) + def test_lb_update_exception(self): - self.helper._find_ovn_lb.side_effect = [RuntimeError] + self.helper._find_ovn_lbs.side_effect = [RuntimeError] status = self.helper.lb_update(self.lb) self.assertEqual(status['loadbalancers'][0]['provisioning_status'], constants.ERROR) @@ -966,7 +1179,7 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): self.helper.ovn_nbdb_api.db_set.call_count) def test_listener_create_exception(self): - self.helper._find_ovn_lb.side_effect = [RuntimeError] + self.helper.ovn_nbdb_api.db_set.side_effect = [RuntimeError] status = self.helper.listener_create(self.listener) self.assertEqual(status['loadbalancers'][0]['provisioning_status'], constants.ACTIVE) @@ -996,14 +1209,23 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): self.assertEqual(status['pools'][0]['provisioning_status'], constants.ACTIVE) - def test_listener_update_exception(self): - self.helper._find_ovn_lb.side_effect = [RuntimeError] + def test_listener_update_row_not_found(self): + self.helper._find_ovn_lbs.side_effect = [idlutils.RowNotFound] + status = self.helper.listener_update(self.listener) + self.assertEqual(status['loadbalancers'][0]['provisioning_status'], + constants.ERROR) + self.assertEqual(status['listeners'][0]['provisioning_status'], + constants.ERROR) + self.helper.ovn_nbdb_api.db_set.assert_not_called() + + @mock.patch.object(ovn_driver.OvnProviderHelper, '_refresh_lb_vips') + def test_listener_update_exception(self, refresh_vips): + refresh_vips.side_effect = [RuntimeError] status = self.helper.listener_update(self.listener) self.assertEqual(status['loadbalancers'][0]['provisioning_status'], constants.ACTIVE) self.assertEqual(status['listeners'][0]['provisioning_status'], constants.ERROR) - self.helper.ovn_nbdb_api.db_set.assert_not_called() @mock.patch.object(ovn_driver.OvnProviderHelper, '_refresh_lb_vips') def test_listener_update_listener_enabled(self, refresh_vips): @@ -1064,8 +1286,18 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): constants.OFFLINE) self.helper.ovn_nbdb_api.db_remove.assert_not_called() + def test_listener_delete_row_not_found(self): + self.helper._find_ovn_lbs.side_effect = [idlutils.RowNotFound] + status = self.helper.listener_delete(self.listener) + self.assertEqual(status['loadbalancers'][0]['provisioning_status'], + constants.ACTIVE) + self.assertEqual(status['listeners'][0]['provisioning_status'], + constants.DELETED) + self.assertEqual(status['listeners'][0]['operating_status'], + constants.OFFLINE) + def test_listener_delete_exception(self): - self.helper._find_ovn_lb.side_effect = [RuntimeError] + self.helper.ovn_nbdb_api.db_remove.side_effect = [RuntimeError] status = self.helper.listener_delete(self.listener) self.assertEqual(status['loadbalancers'][0]['provisioning_status'], constants.ACTIVE) @@ -1090,6 +1322,41 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): refresh_vips.assert_called_once_with( self.ovn_lb.uuid, self.ovn_lb.external_ids) + @mock.patch.object(ovn_driver.OvnProviderHelper, '_is_lb_empty') + def test_listener_delete_ovn_lb_not_empty(self, lb_empty): + lb_empty.return_value = False + self.helper.listener_delete(self.listener) + self.helper.ovn_nbdb_api.db_remove.assert_called_once_with( + 'Load_Balancer', self.ovn_lb.uuid, + 'external_ids', 'listener_%s' % self.listener_id) + self.helper.ovn_nbdb_api.lb_del.assert_not_called() + + @mock.patch.object(ovn_driver.OvnProviderHelper, '_is_lb_empty') + def test_listener_delete_ovn_lb_empty_lb_empty(self, lb_empty): + lb_empty.return_value = True + self.helper.listener_delete(self.listener) + self.helper.ovn_nbdb_api.db_remove.assert_called_once_with( + 'Load_Balancer', self.ovn_lb.uuid, + 'external_ids', 'listener_%s' % self.listener_id) + self.helper.ovn_nbdb_api.lb_del.assert_not_called() + # Assert that protocol has been set to []. + self.helper.ovn_nbdb_api.db_set.assert_has_calls([ + mock.call('Load_Balancer', self.ovn_lb.uuid, + ('protocol', []))]) + + @mock.patch.object(ovn_driver.OvnProviderHelper, '_is_lb_empty') + def test_listener_delete_ovn_lb_empty_lb_not_empty(self, lb_empty): + self.mock_find_ovn_lbs.stop() + self.helper.ovn_nbdb_api.db_find_rows.return_value.\ + execute.side_effect = [[self.ovn_lb], []] + lb_empty.return_value = True + self.helper.listener_delete(self.listener) + self.helper.ovn_nbdb_api.db_remove.assert_called_once_with( + 'Load_Balancer', self.ovn_lb.uuid, + 'external_ids', 'listener_%s' % self.listener_id) + self.helper.ovn_nbdb_api.lb_del.assert_called_once_with( + self.ovn_lb.uuid) + def test_pool_create(self): status = self.helper.pool_create(self.pool) self.assertEqual(status['loadbalancers'][0]['provisioning_status'], @@ -1111,7 +1378,7 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): constants.OFFLINE) def test_pool_create_exception(self): - self.helper._find_ovn_lb.side_effect = [RuntimeError] + self.helper.ovn_nbdb_api.db_set.side_effect = [RuntimeError] status = self.helper.pool_update(self.pool) self.assertEqual(status['pools'][0]['provisioning_status'], constants.ERROR) @@ -1223,15 +1490,23 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): self.helper.ovn_nbdb_api.db_set.assert_has_calls( expected_calls) + def test_pool_delete_row_not_found(self): + self.helper._find_ovn_lbs.side_effect = [idlutils.RowNotFound] + status = self.helper.pool_delete(self.pool) + self.assertEqual(status['loadbalancers'][0]['provisioning_status'], + constants.ACTIVE) + self.assertEqual(status['pools'][0]['provisioning_status'], + constants.DELETED) + self.helper.ovn_nbdb_api.db_remove.assert_not_called() + self.helper.ovn_nbdb_api.db_set.assert_not_called() + def test_pool_delete_exception(self): - self.helper._find_ovn_lb.side_effect = [RuntimeError] + self.helper.ovn_nbdb_api.db_set.side_effect = [RuntimeError] status = self.helper.pool_delete(self.pool) self.assertEqual(status['loadbalancers'][0]['provisioning_status'], constants.ACTIVE) self.assertEqual(status['pools'][0]['provisioning_status'], constants.ERROR) - self.helper.ovn_nbdb_api.db_remove.assert_not_called() - self.helper.ovn_nbdb_api.db_set.assert_not_called() def test_pool_delete_associated_listeners(self): self.helper._get_pool_listeners.return_value = ['listener1'] @@ -1263,6 +1538,41 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): 'Load_Balancer', self.ovn_lb.uuid, 'external_ids', 'pool_%s:D' % self.pool_id) + @mock.patch.object(ovn_driver.OvnProviderHelper, '_is_lb_empty') + def test_pool_delete_ovn_lb_not_empty(self, lb_empty): + lb_empty.return_value = False + self.helper.pool_delete(self.pool) + self.helper.ovn_nbdb_api.db_remove.assert_called_once_with( + 'Load_Balancer', self.ovn_lb.uuid, + 'external_ids', 'pool_%s' % self.pool_id) + self.helper.ovn_nbdb_api.lb_del.assert_not_called() + + @mock.patch.object(ovn_driver.OvnProviderHelper, '_is_lb_empty') + def test_pool_delete_ovn_lb_empty_lb_empty(self, lb_empty): + lb_empty.return_value = True + self.helper.pool_delete(self.pool) + self.helper.ovn_nbdb_api.db_remove.assert_called_once_with( + 'Load_Balancer', self.ovn_lb.uuid, + 'external_ids', 'pool_%s' % self.pool_id) + self.helper.ovn_nbdb_api.lb_del.assert_not_called() + # Assert that protocol has been set to []. + self.helper.ovn_nbdb_api.db_set.assert_called_with( + 'Load_Balancer', self.ovn_lb.uuid, + ('protocol', [])) + + @mock.patch.object(ovn_driver.OvnProviderHelper, '_is_lb_empty') + def test_pool_delete_ovn_lb_empty_lb_not_empty(self, lb_empty): + self.mock_find_ovn_lbs.stop() + self.helper.ovn_nbdb_api.db_find_rows.return_value.\ + execute.side_effect = [[self.ovn_lb], []] + lb_empty.return_value = True + self.helper.pool_delete(self.pool) + self.helper.ovn_nbdb_api.db_remove.assert_called_once_with( + 'Load_Balancer', self.ovn_lb.uuid, + 'external_ids', 'pool_%s' % self.pool_id) + self.helper.ovn_nbdb_api.lb_del.assert_called_once_with( + self.ovn_lb.uuid) + def test_member_create(self): self.ovn_lb.external_ids = mock.MagicMock() status = self.helper.member_create(self.member) @@ -1959,7 +2269,7 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): 'info': {'action': 'associate', 'vip_fip': '10.0.0.1', - 'lb_id': 'foo'}, + 'ovn_lb': self.ovn_lb}, 'type': 'handle_vip_fip'} self.mock_add_request.assert_called_once_with(expected_call) @@ -1985,7 +2295,7 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): 'info': {'action': 'disassociate', 'vip_fip': None, - 'lb_id': 'foo'}, + 'ovn_lb': self.ovn_lb}, 'type': 'handle_vip_fip'} self.mock_add_request.assert_called_once_with(expected_call) @@ -2000,7 +2310,7 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): self.mock_add_request.assert_not_called() @mock.patch('ovn_octavia_provider.driver.OvnProviderHelper.' - '_find_ovn_lb') + '_find_ovn_lbs') def test_vip_port_update_handler_lb_not_found(self, lb): lb.side_effect = [idlutils.RowNotFound] self.switch_port_event = ovn_driver.LogicalSwitchPortUpdateEvent( @@ -2014,13 +2324,39 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): self.mock_add_request.assert_not_called() @mock.patch('ovn_octavia_provider.driver.OvnProviderHelper.' - '_find_ovn_lb') + '_find_ovn_lbs') + def test_vip_port_update_handler_multiple_lbs(self, lb): + lb1 = mock.MagicMock() + lb2 = mock.MagicMock() + lb.return_value = [lb1, lb2] + self.switch_port_event = ovn_driver.LogicalSwitchPortUpdateEvent( + self.helper) + port_name = '%s%s' % (ovn_const.LB_VIP_PORT_PREFIX, 'foo') + attrs = {'external_ids': + {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: port_name}} + row = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs=attrs) + self.switch_port_event.run(mock.ANY, row, mock.ANY) + + def expected_call(lb): + return {'type': 'handle_vip_fip', + 'info': + {'action': mock.ANY, + 'vip_fip': None, + 'ovn_lb': lb}} + + self.mock_add_request.assert_has_calls([ + mock.call(expected_call(lb1)), + mock.call(expected_call(lb2))]) + + @mock.patch('ovn_octavia_provider.driver.OvnProviderHelper.' + '_find_ovn_lbs') def test_handle_vip_fip_disassociate(self, flb): + lb = mock.MagicMock() fip_info = { 'action': 'disassociate', 'vip_fip': None, - 'lb_id': 'foo'} - lb = mock.MagicMock() + 'ovn_lb': lb} flb.return_value = lb self.helper.handle_vip_fip(fip_info) calls = [ @@ -2031,13 +2367,14 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): self.helper.ovn_nbdb_api.assert_has_calls(calls) @mock.patch('ovn_octavia_provider.driver.OvnProviderHelper.' - '_find_ovn_lb') + '_find_ovn_lbs') @mock.patch('ovn_octavia_provider.driver.get_network_driver') def test_handle_vip_fip_associate(self, net_dr, fb): + lb = mock.MagicMock() fip_info = { 'action': 'associate', 'vip_fip': '10.0.0.123', - 'lb_id': 'foo'} + 'ovn_lb': lb} members = 'member_%s_%s:%s' % (self.member_id, self.member_address, self.member_port) @@ -2046,7 +2383,6 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): 'pool_%s' % self.pool_id: members, 'neutron:vip': '172.26.21.20'} - lb = mock.MagicMock() lb.external_ids = external_ids fb.return_value = lb @@ -2062,15 +2398,6 @@ class TestOvnProviderHelper(TestOvnOctaviaBase): '172.26.21.20:80': '192.168.2.149:1010'}))] self.helper.ovn_nbdb_api.assert_has_calls(calls) - @mock.patch('ovn_octavia_provider.driver.OvnProviderHelper.' - '_find_ovn_lb') - @mock.patch('ovn_octavia_provider.driver.get_network_driver') - def test_handle_vip_fip_lb_not_found(self, net_dr, fb): - fip_info = {'lb_id': 'foo'} - fb.side_effect = [idlutils.RowNotFound] - self.helper.handle_vip_fip(fip_info) - self.helper.ovn_nbdb_api.assert_not_called() - @mock.patch.object(ovn_driver, 'atexit') def test_ovsdb_connections(self, mock_atexit): ovn_driver.OvnProviderHelper.ovn_nbdb_api = None diff --git a/releasenotes/notes/ovn-octavia-provider-driver-multiple-protocols-4a93e184b8f374c7.yaml b/releasenotes/notes/ovn-octavia-provider-driver-multiple-protocols-4a93e184b8f374c7.yaml new file mode 100644 index 00000000..238be2b8 --- /dev/null +++ b/releasenotes/notes/ovn-octavia-provider-driver-multiple-protocols-4a93e184b8f374c7.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + OVN Octavia provider driver now supports both TCP and UDP + pool/listener protocols configured in the same Octavia Load Balancer.