Browse Source

Add a new Event Handler for Octavia

This patch adds a new event handler for Octavia so that in
the event that a network is added to a router, the loadbalancer
on the network are also applied to the router and vice versa.

The process involves:
- Adding a new connection class for Octavia to communicate with
  OVN NB DB, setting up the connection and communicating with
  the IDL.
- Setting up events for Router interface addition and deletion
- Updating Loadbalancer tables and Router/Switch tables based
  upon the generated events.
- Add network UUID to LRP external_ids

Co-Authored-By: Terry Wilson <twilson@redhat.com>
Co-Authored-By: Maciej Józefczyk <mjozefcz@redhat.com>

Change-Id: If10016325517e4a1fc5f7cfac7f7bd58deb09b54
Closes-Bug: #1794260
tags/7.0.0.0b1
reedip Maciej Józefczyk 9 months ago
parent
commit
aa4268a094
8 changed files with 1046 additions and 232 deletions
  1. +7
    -2
      doc/source/contributor/design/loadbalancer.rst
  2. +3
    -0
      networking_ovn/common/constants.py
  3. +3
    -1
      networking_ovn/common/ovn_client.py
  4. +1
    -1
      networking_ovn/common/utils.py
  5. +325
    -179
      networking_ovn/octavia/ovn_driver.py
  6. +181
    -19
      networking_ovn/tests/functional/octavia/test_ovn_driver.py
  7. +11
    -4
      networking_ovn/tests/unit/l3/test_l3_ovn.py
  8. +515
    -26
      networking_ovn/tests/unit/octavia/test_ovn_driver.py

+ 7
- 2
doc/source/contributor/design/loadbalancer.rst View File

@@ -258,9 +258,14 @@ As explained earlier in the design section:
- If a network N1 has a LoadBalancer LB1 associated to it and one of
its interfaces is added to a router R1, LB1 is associated with R1 as well.

- If a network N2 has a loadBalancer LB2 and one of its interfaces is added
- If a network N2 has a LoadBalancer LB2 and one of its interfaces is added
to the router R1, then R1 will have both LoadBalancers LB1 and LB2. N1 and
N2 will also have both LoadBalancers associated to them.
N2 will also have both the LoadBalancers associated to them. However, kindly
note that though network N1 would have both LB1 and LB2 LoadBalancers
associated with it, only LB1 would be the LoadBalancer which has a direct
reference to the network N1, since LB1 was created on N1. This is visible
in the ``ls_ref`` key of the ``external_ids`` column in LB1's entry in
the ``load_balancer`` table.

- If a network N3 is added to the router R1, N3 will also have both
LoadBalancers (LB1, LB2) associated to it.


+ 3
- 0
networking_ovn/common/constants.py View File

@@ -153,6 +153,9 @@ MAINTENANCE_DELETE_TYPE_ORDER = {
# peer router port (connecting to the logical router).
DEFAULT_ADDR_FOR_LSP_WITH_PEER = 'router'

# Loadbalancer constants
LRP_PREFIX = "lrp-"

# Hash Ring constants
HASH_RING_NODES_TIMEOUT = 60
HASH_RING_CACHE_TIMEOUT = 30

+ 3
- 1
networking_ovn/common/ovn_client.py View File

@@ -1129,7 +1129,9 @@ class OVNClient(object):
ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(utils.get_revision_number(
port, ovn_const.TYPE_ROUTER_PORTS)),
ovn_const.OVN_SUBNET_EXT_IDS_KEY:
' '.join(utils.get_port_subnet_ids(port))}
' '.join(utils.get_port_subnet_ids(port)),
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY:
utils.ovn_name(port['network_id'])}

router_id = port.get('device_id')
if router_id:


+ 1
- 1
networking_ovn/common/utils.py View File

@@ -54,7 +54,7 @@ def ovn_lrouter_port_name(id):
# - patch-lrp-<UUID>-to-<UUID>
# - patch-<UUID>-to-lrp-<UUID>
# lrp stands for Logical Router Port
return 'lrp-%s' % id
return constants.LRP_PREFIX + '%s' % id


def ovn_provnet_port_name(network_id):


+ 325
- 179
networking_ovn/octavia/ovn_driver.py View File

@@ -16,6 +16,7 @@ import atexit
import copy
import threading

from neutronclient.common import exceptions as n_exc
from octavia_lib.api.drivers import data_models as o_datamodels
from octavia_lib.api.drivers import driver_lib as o_driver_lib
from octavia_lib.api.drivers import exceptions as driver_exceptions
@@ -25,147 +26,24 @@ from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
from ovs.stream import Stream
from ovsdbapp.backend.ovs_idl import connection
from ovsdbapp.backend.ovs_idl import event as row_event
from ovsdbapp.backend.ovs_idl import idlutils
from ovsdbapp.schema.ovn_northbound import impl_idl as idl_ovn
from six.moves import queue as Queue
from stevedore import driver
import tenacity

from networking_ovn._i18n import _
from networking_ovn.common import config as ovn_cfg
from networking_ovn.common import constants as ovn_const
from networking_ovn.common import utils as ovn_utils
from networking_ovn.ovsdb import ovsdb_monitor

CONF = cfg.CONF # Gets Octavia Conf as it runs under o-api domain

LOG = logging.getLogger(__name__)

"""
OVN Northbound schema [1] has a table to store load balancers.
The table looks like

"Load_Balancer": {
"columns": {
"name": {"type": "string"},
"vips": {
"type": {"key": "string", "value": "string",
"min": 0, "max": "unlimited"}},
"protocol": {
"type": {"key": {"type": "string",
"enum": ["set", ["tcp", "udp"]]},
"min": 0, "max": 1}},
"external_ids": {
"type": {"key": "string", "value": "string",
"min": 0, "max": "unlimited"}}},
"isRoot": true},

Logical_Switch table (which corresponds to neutron network) has a column
'load_balancers' which refers to the 'Load_Balancer' table.

This driver updates this database. When a load balancer is created, a row in
this table is created. And when the listeners and members are added, 'vips'
column is updated accordingly. And the Logical_Switch's 'load_balancers'
column is also updated accordingly.

ovn-northd service which monitors for changes to the OVN Northbound db,
generates OVN logical flows to enable load balancing and ovn-controller running
on each compute node, translates the logical flows into actual OF Flows.
[1] - https://github.com/openvswitch/ovs/blob/
d1b235d7a6246e00d4afc359071d3b6b3ed244c3/ovn/ovn-nb.ovsschema#L117.

Below are few examples on what happens when loadbalancer commands are
executed and what changes in the Load_Balancer Northbound db table.

1. Create a load balancer
$openstack loadbalancer create --provider ovn --vip-subnet-id=private lb1


$ ovn-nbctl list load_balancer

_uuid : 9dd65bae-2501-43f2-b34e-38a9cb7e4251
external_ids : {
lr_ref="neutron-52b6299c-6e38-4226-a275-77370296f257",
ls_refs="{\"neutron-2526c68a-5a9e-484c-8e00-0716388f6563\": 1}",
neutron:vip="10.0.0.10",
neutron:vip_port_id="2526c68a-5a9e-484c-8e00-0716388f6563"}

name : "973a201a-8787-4f6e-9b8f-ab9f93c31f44"
protocol : []
vips : {}


2. Create a pool
$openstack loadbalancer pool create --name p1 --loadbalancer lb1
--protocol TCP --lb-algorithm ROUND_ROBIN

$ovn-nbctl list load_balancer
_uuid : 9dd65bae-2501-43f2-b34e-38a9cb7e4251
external_ids : {
lr_ref="neutron-52b6299c-6e38-4226-a275-77370296f257",
ls_refs="{\"neutron-2526c68a-5a9e-484c-8e00-0716388f6563\": 1}",
"pool_f2ddf7a6-4047-4cc9-97be-1d1a6c47ece9"="", neutron:vip="10.0.0.10",
neutron:vip_port_id="2526c68a-5a9e-484c-8e00-0716388f6563"}
name : "973a201a-8787-4f6e-9b8f-ab9f93c31f44"
protocol : []
vips : {}

3. Create a member
$openstack loadbalancer member create --address 10.0.0.107
--subnet-id 2d54ec67-c589-473b-bc67-41f3d1331fef --protocol-port 80 p1

$ovn-nbctl list load_balancer
_uuid : 9dd65bae-2501-43f2-b34e-38a9cb7e4251
external_ids : {
lr_ref="neutron-52b6299c-6e38-4226-a275-77370296f257",
ls_refs="{\"neutron-2526c68a-5a9e-484c-8e00-0716388f6563\": 2}",
"pool_f2ddf7a6-4047-4cc9-97be-1d1a6c47ece9"=
"member_579c0c9f-d37d-4ba5-beed-cabf6331032d_10.0.0.107:80",
neutron:vip="10.0.0.10",
neutron:vip_port_id="2526c68a-5a9e-484c-8e00-0716388f6563"}
name : "973a201a-8787-4f6e-9b8f-ab9f93c31f44"
protocol : []
vips : {}

4. Create another member

$openstack loadbalancer member create --address 20.0.0.107
--subnet-id c2e2da10-1217-4fe2-837a-1c45da587df7 --protocol-port 80 p1

$ovn-nbctl list load_balancer
_uuid : 9dd65bae-2501-43f2-b34e-38a9cb7e4251
external_ids : {
lr_ref="neutron-52b6299c-6e38-4226-a275-77370296f257",
ls_refs="{\"neutron-2526c68a-5a9e-484c-8e00-0716388f6563\": 2,
\"neutron-12c42705-3e15-4e2d-8fc0-070d1b80b9ef\": 1}",
"pool_f2ddf7a6-4047-4cc9-97be-1d1a6c47ece9"=
"member_579c0c9f-d37d-4ba5-beed-cabf6331032d_10.0.0.107:80,
member_d100f2ed-9b55-4083-be78-7f203d095561_20.0.0.107:80",
neutron:vip="10.0.0.10",
neutron:vip_port_id="2526c68a-5a9e-484c-8e00-0716388f6563"}
name : "973a201a-8787-4f6e-9b8f-ab9f93c31f44"
protocol : []
vips : {}

5. Create a listener

$openstack loadbalancer listener create --name l1 --protocol TCP
--protocol-port 82 --default-pool p1 lb1

$ovn-nbctl list load_balancer
_uuid : 9dd65bae-2501-43f2-b34e-38a9cb7e4251
external_ids : {
lr_ref="neutron-52b6299c-6e38-4226-a275-77370296f257",
ls_refs="{\"neutron-2526c68a-5a9e-484c-8e00-0716388f6563\": 2,
\"neutron-12c42705-3e15-4e2d-8fc0-070d1b80b9ef\": 1}",
"pool_f2ddf7a6-4047-4cc9-97be-1d1a6c47ece9"="10.0.0.107:80,20.0.0.107:80",
"listener_12345678-2501-43f2-b34e-38a9cb7e4132"=
"82:pool_f2ddf7a6-4047-4cc9-97be-1d1a6c47ece9",
neutron:vip="10.0.0.10",
neutron:vip_port_id="2526c68a-5a9e-484c-8e00-0716388f6563"}
name : "973a201a-8787-4f6e-9b8f-ab9f93c31f44"
protocol : []
vips : {"10.0.0.10:82"="10.0.0.107:80,20.0.0.107:80"}

"""


REQ_TYPE_LB_CREATE = 'lb_create'
REQ_TYPE_LB_DELETE = 'lb_delete'
REQ_TYPE_LB_FAILOVER = 'lb_failover'
@@ -179,6 +57,9 @@ REQ_TYPE_POOL_UPDATE = 'pool_update'
REQ_TYPE_MEMBER_CREATE = 'member_create'
REQ_TYPE_MEMBER_DELETE = 'member_delete'
REQ_TYPE_MEMBER_UPDATE = 'member_update'
REQ_TYPE_LB_CREATE_LRP_ASSOC = 'lb_create_lrp_assoc'
REQ_TYPE_LB_DELETE_LRP_ASSOC = 'lb_delete_lrp_assoc'

REQ_TYPE_EXIT = 'exit'

DISABLED_RESOURCE_SUFFIX = 'D'
@@ -206,6 +87,69 @@ def get_network_driver():
).driver


class LogicalRouterPortEvent(row_event.RowEvent):

driver = None

def __init__(self, driver):
table = 'Logical_Router_Port'
events = (self.ROW_CREATE, self.ROW_DELETE)
super(LogicalRouterPortEvent, self).__init__(
events, table, None)
self.event_name = 'LogicalRouterPortEvent'
self.driver = driver

def run(self, event, row, old):
LOG.debug('LogicalRouterPortEvent logged, '
'%(event)s, %(row)s',
{'event': event,
'row': row})
if not self.driver or row.gateway_chassis:
return
if event == self.ROW_CREATE:
self.driver.lb_create_lrp_assoc_handler(row)
elif event == self.ROW_DELETE:
self.driver.lb_delete_lrp_assoc_handler(row)


class OvnNbIdlForLb(ovsdb_monitor.OvnIdl):

SCHEMA = "OVN_Northbound"

def __init__(self):
self.tables = ('Logical_Switch', 'Load_Balancer', 'Logical_Router',
'Logical_Switch_Port', 'Logical_Router_Port',
'Gateway_Chassis')
self.conn_string = ovn_cfg.get_ovn_nb_connection()
helper = self._get_ovsdb_helper(self.conn_string)
for table in self.tables:
helper.register_table(table)
super(OvnNbIdlForLb, self).__init__(
driver=None, remote=self.conn_string, schema=helper)
self.event_lock_name = "neutron_ovn_octavia_event_lock"

@tenacity.retry(
wait=tenacity.wait_exponential(max=180),
reraise=True)
def _get_ovsdb_helper(self, connection_string):
return idlutils.get_schema_helper(connection_string, self.SCHEMA)

def start(self):
self.conn = connection.Connection(
self, timeout=ovn_cfg.get_ovn_ovsdb_timeout())
return idl_ovn.OvnNbApiIdlImpl(self.conn)

def stop(self):
# Close the running connection
if not self.conn.stop(timeout=ovn_cfg.get_ovn_ovsdb_timeout()):
LOG.debug("Connection terminated to OvnNb "
"but a thread is still alive")
# complete the shutdown for the event handler
self.notify_handler.shutdown()
# Close the idl session
self.close()


class OvnProviderHelper(object):

ovn_nbdb_api = None
@@ -216,7 +160,7 @@ class OvnProviderHelper(object):
self.helper_thread.daemon = True
atexit.register(self.shutdown)
self._octavia_driver_lib = o_driver_lib.DriverLibrary()
self._init_ovnnb_db_api()
self._check_and_set_ssl_files()
self._init_lb_actions()
self.start()

@@ -235,24 +179,15 @@ class OvnProviderHelper(object):
REQ_TYPE_MEMBER_CREATE: self.member_create,
REQ_TYPE_MEMBER_DELETE: self.member_delete,
REQ_TYPE_MEMBER_UPDATE: self.member_update,
REQ_TYPE_LB_CREATE_LRP_ASSOC: self.lb_create_lrp_assoc,
REQ_TYPE_LB_DELETE_LRP_ASSOC: self.lb_delete_lrp_assoc,
}

def _init_ovnnb_db_api(self):
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
if OvnProviderHelper.ovn_nbdb_api:
return

tables = ('Logical_Switch', 'Load_Balancer', 'Logical_Router',
'Logical_Switch_Port', 'Logical_Router_Port')
conn = ovn_utils.get_ovsdb_connection(
ovn_cfg.get_ovn_nb_connection(), 'OVN_Northbound',
ovn_cfg.get_ovn_ovsdb_timeout(), tables=tables)

# idl_ovn.OvnNbApiIdlImpl.ovsdb_connection is a class variable.
# So set it to None so that we don't use any old connection object.
# See ovsdbapp.backend.ovs_idl.Backend class for more details.
idl_ovn.OvnNbApiIdlImpl.ovsdb_connection = None
OvnProviderHelper.ovn_nbdb_api = idl_ovn.OvnNbApiIdlImpl(conn)

priv_key_file = ovn_cfg.get_ovn_nb_private_key()
cert_file = ovn_cfg.get_ovn_nb_certificate()
ca_cert_file = ovn_cfg.get_ovn_nb_ca_cert()
@@ -266,8 +201,131 @@ class OvnProviderHelper(object):
Stream.ssl_set_ca_cert_file(ca_cert_file)

def start(self):
self.ovn_nb_idl_for_lb = OvnNbIdlForLb()
OvnProviderHelper.ovn_nbdb_api = self.ovn_nb_idl_for_lb.start()
events = [LogicalRouterPortEvent(self)]
self.ovn_nb_idl_for_lb.notify_handler.watch_events(events)
self.helper_thread.start()

def shutdown(self):
self.requests.put({'type': REQ_TYPE_EXIT})
self.helper_thread.join()
self.ovn_nb_idl_for_lb.stop()

@staticmethod
def _map_val(row, col, key):
# If the row doesnt exist, RowNotFound is raised by the _map_val
# and is expected to be caught by the caller.
try:
return getattr(row, col)[key]
except KeyError:
raise idlutils.RowNotFound(table=row._table.name,
col=col, match=key)

def _get_nw_router_info_on_interface_event(self, lrp):
"""Get the Router and Network information on an interface event

This function is called when a new interface between a router and
a network is added or deleted.
Input: Logical Router Port row which is coming from
LogicalRouterPortEvent.
Output: A row from router table and network table matching the router
and network for which the event was generated.
Exception: RowNotFound exception can be generated.
"""
router = self.ovn_nbdb_api.lookup(
'Logical_Router', ovn_utils.ovn_name(self._map_val(
lrp, 'external_ids', ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY)))
network = self.ovn_nbdb_api.lookup(
'Logical_Switch',
self._map_val(lrp, 'external_ids',
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY))
return router, network

def lb_delete_lrp_assoc_handler(self, row):
try:
router, network = self._get_nw_router_info_on_interface_event(row)
except idlutils.RowNotFound:
LOG.debug("Router or network information not found")
return
request_info = {'network': network,
'router': router}
self.add_request({'type': REQ_TYPE_LB_DELETE_LRP_ASSOC,
'info': request_info})

def lb_delete_lrp_assoc(self, info):
# TODO(reedip): When OVS>=2.12, LB can be deleted without removing
# Network and Router references as pushed in the patch
# https://github.com/openvswitch/ovs/commit
# /612f80fa8ebf88dad2e204364c6c02b451dca36c
commands = []
network = info['network']
router = info['router']

# Find all loadbalancers which have a reference with the network
nw_lb = self._find_lb_in_ls(network=network)
# Find all loadbalancers which have a reference with the router
r_lb = set(router.load_balancer) - nw_lb
# Delete all LB on N/W from Router
for nlb in nw_lb:
commands.extend(self._update_lb_to_lr_association(nlb, router,
delete=True))
# Delete all LB on Router from N/W
for rlb in r_lb:
commands.append(self.ovn_nbdb_api.ls_lb_del(
network.uuid, rlb.uuid))
if commands:
self._execute_commands(commands)

def lb_create_lrp_assoc_handler(self, row):
try:
router, network = self._get_nw_router_info_on_interface_event(row)
except idlutils.RowNotFound:
LOG.debug("Router or network information not found")
return
request_info = {'network': network,
'router': router}
self.add_request({'type': REQ_TYPE_LB_CREATE_LRP_ASSOC,
'info': request_info})

def lb_create_lrp_assoc(self, info):
commands = []

router_lb = set(info['router'].load_balancer)
network_lb = set(info['network'].load_balancer)
# Add only those lb to routers which are unique to the network
for lb in (network_lb - router_lb):
commands.extend(self._update_lb_to_lr_association(
lb, info['router']))

# Add those lb to the network which are unique to the router
for lb in (router_lb - network_lb):
commands.append(self.ovn_nbdb_api.ls_lb_add(
info['network'].uuid, lb.uuid, may_exist=True))
if commands:
self._execute_commands(commands)

def _find_lb_in_ls(self, network):
"""Find LB associated to a Network using Network information

This function retrieves those loadbalancers whose ls_ref
column in the OVN northbound database's load_balancer table
has the network's name. Though different networks can be
associated with a loadbalancer, but ls_ref of a loadbalancer
points to the network where it was actually created, and this
function tries to retrieve all those loadbalancers created on this
network.
Input : row of type Logical_Switch
Output: set of rows of type Load_Balancer or empty set
"""
return {lb for lb in network.load_balancer
if network.name in lb.external_ids.get(LB_EXT_IDS_LS_REFS_KEY,
[])}

def _find_lb_in_table(self, lb, table):
return [item for item in self.ovn_nbdb_api.tables[table].rows.values()
if lb in item.load_balancer]

def request_handler(self):
while True:
try:
@@ -290,26 +348,27 @@ class OvnProviderHelper(object):
def add_request(self, req):
self.requests.put(req)

def shutdown(self):
self.requests.put({'type': REQ_TYPE_EXIT})

def _update_status_to_octavia(self, status):
try:
self._octavia_driver_lib.update_loadbalancer_status(status)
except driver_exceptions.UpdateStatusError as e:
# TODO(numans): Handle the exception properly
LOG.error('Error while updating the load balancer status: %s',
e.fault_string)

def _find_ovn_lb(self, loadbalancer_id):
find_condition = ('name', '=', loadbalancer_id)
ovn_lb = self.ovn_nbdb_api.db_find(
'Load_Balancer', find_condition, row=True).execute(
check_error=True)
if len(ovn_lb) > 1:
LOG.warning("Two or more load balancer named '%s' "
"have been found in the database", loadbalancer_id)
return ovn_lb[0] if len(ovn_lb) == 1 else None
msg = ("Error while updating the load balancer "
"status: %s") % e.fault_string
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

This function searches for the LoadBalancer whose Name has the pattern
passed in lb_id.
Input: String format of 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.
"""
return self.ovn_nbdb_api.lookup('Load_Balancer', lb_id)

def _find_ovn_lb_with_pool_key(self, pool_key):
lbs = self.ovn_nbdb_api.db_list_rows('Load_Balancer').execute()
@@ -324,6 +383,11 @@ class OvnProviderHelper(object):

def _update_lb_to_ls_association(self, ovn_lb, network_id=None,
subnet_id=None, associate=True):
"""Update LB association with Logical Switch

This function deals with updating the References of Logical Switch
in LB and addition of LB to LS.
"""
commands = []
if not network_id and not subnet_id:
return commands
@@ -377,19 +441,80 @@ class OvnProviderHelper(object):

return commands

def _update_lb_to_lr_association(self, ovn_lb, ovn_lr):
def _del_lb_to_lr_association(self, ovn_lb, ovn_lr, lr_ref):
commands = []
if lr_ref:
try:
lr_ref = [r for r in
[lr.strip() for lr in lr_ref.split(',')]
if r != ovn_lr.name]
except ValueError:
msg = ('The loadbalancer %(lb)s is not associated with '
'the router %(router)s' %
{'lb': ovn_lb.name,
'router': ovn_lr.name})
LOG.warning(msg)
if lr_ref:
commands.append(
self.ovn_nbdb_api.db_set(
'Load_Balancer', ovn_lb.uuid,
('external_ids',
{LB_EXT_IDS_LR_REF_KEY: ','.join(lr_ref)})))
else:
commands.append(
self.ovn_nbdb_api.db_remove(
'Load_Balancer', ovn_lb.uuid, 'external_ids',
(LB_EXT_IDS_LR_REF_KEY))
)
commands.append(
self.ovn_nbdb_api.lr_lb_del(ovn_lr.uuid, ovn_lb.uuid,
if_exists=True)
)
for net in self._find_ls_for_lr(ovn_lr):
commands.append(self.ovn_nbdb_api.ls_lb_del(
net, ovn_lb.uuid, if_exists=True))
return commands

def _add_lb_to_lr_association(self, ovn_lb, ovn_lr, lr_rf):
commands = []
lr_ref = {LB_EXT_IDS_LR_REF_KEY: ovn_lr.name}
commands.append(
self.ovn_nbdb_api.db_set('Load_Balancer', ovn_lb.uuid,
('external_ids', lr_ref))
self.ovn_nbdb_api.lr_lb_add(ovn_lr.uuid, ovn_lb.uuid,
may_exist=True)
)
for net in self._find_ls_for_lr(ovn_lr):
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.lr_lb_add(ovn_lr.uuid, ovn_lb.uuid)
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):
lr_ref = ovn_lb.external_ids.get(LB_EXT_IDS_LR_REF_KEY)
if delete:
return self._del_lb_to_lr_association(ovn_lb, ovn_lr, lr_ref)
else:
return self._add_lb_to_lr_association(ovn_lb, ovn_lr, lr_ref)

def _find_ls_for_lr(self, router):
# NOTE(mjozefcz): We skip here ports connected to
# provider networks (with gateway_chassis set).
netdriver = get_network_driver()
try:
return [ovn_utils.ovn_name(netdriver.get_subnet(sid).network_id)
for port in router.ports
for sid in port.external_ids.get(
ovn_const.OVN_SUBNET_EXT_IDS_KEY, '').split(' ')
if not port.gateway_chassis]
except Exception:
LOG.exception('Unknown exception occurred')
return []

def _find_lr_of_ls(self, ovn_ls):
lsp_router_port = None
for port in ovn_ls.ports or []:
@@ -407,6 +532,10 @@ class OvnProviderHelper(object):
for lrp in lr.ports:
if lrp.name == lrp_name:
return lr
# Handles networks with only gateway port in the router
if ovn_utils.ovn_lrouter_port_name(
lr.external_ids.get("neutron:gw_port_id")) == lrp_name:
return lr

def _get_listener_key(self, listener_id, is_enabled=True):
listener_key = LB_EXT_IDS_LISTENER_PREFIX + str(listener_id)
@@ -541,6 +670,8 @@ class OvnProviderHelper(object):
ovn_lb, ovn_lr))
self._execute_commands(commands)
operating_status = constants.ONLINE
# The issue is that since OVN doesnt support any HMs,
# we ideally should never put the status as 'ONLINE'
if not loadbalancer.get('admin_state_up', True):
operating_status = constants.OFFLINE
status = {
@@ -567,6 +698,8 @@ class OvnProviderHelper(object):
return status

def lb_delete(self, loadbalancer):
commands = []
port_id = None
try:
status = {'loadbalancers': [{"id": loadbalancer['id'],
"provisioning_status": "DELETED",
@@ -574,7 +707,7 @@ class OvnProviderHelper(object):
ovn_lb = None
try:
ovn_lb = self._find_ovn_lb(loadbalancer['id'])
except IndexError:
except idlutils.RowNotFound:
LOG.warning("Loadbalancer %s not found in OVN Northbound DB."
"Setting the Loadbalancer status to DELETED "
"in Octavia", str(loadbalancer['id']))
@@ -619,17 +752,19 @@ class OvnProviderHelper(object):
# Delete the VIP Port
self.delete_vip_port(ovn_lb.external_ids[
LB_EXT_IDS_VIP_PORT_ID_KEY])
commands = []
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,
if_exists=True)
self.ovn_nbdb_api.ls_lb_del(ovn_ls.uuid, ovn_lb.uuid)
)

lr_ref = ovn_lb.external_ids.get(LB_EXT_IDS_LR_REF_KEY)
# 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():
@@ -637,14 +772,22 @@ class OvnProviderHelper(object):
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
# 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(
ovn_lb.external_ids[LB_EXT_IDS_VIP_PORT_ID_KEY])
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)
except Exception:
LOG.exception(EXCEPTION_MSG, "deletion of loadbalancer")
status = {
@@ -1017,7 +1160,10 @@ class OvnProviderHelper(object):
commands.extend(
self._refresh_lb_vips(ovn_lb.uuid, external_ids))
self._execute_commands(commands)
if pool['admin_state_up']:
if pool['admin_state_up'] and not external_ids.get(pool_key):
# Operating status can be ONLINE if members are present and
# admin_state_up is True. If either is false, operating_status
# is OFFLINE
operating_status = constants.ONLINE
else:
operating_status = constants.OFFLINE
@@ -1134,7 +1280,7 @@ class OvnProviderHelper(object):
self._execute_commands(commands)
return pool_status
else:
msg = _("Member %s not found in the pool") % member['id']
msg = "Member %s not found in the pool" % member['id']
raise driver_exceptions.DriverError(
user_fault_string=msg,
operator_fault_string=msg)


+ 181
- 19
networking_ovn/tests/functional/octavia/test_ovn_driver.py View File

@@ -16,15 +16,19 @@
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
from octavia_lib.api.drivers import exceptions as o_exceptions
from octavia_lib.common import constants as o_constants
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from ovsdbapp.schema.ovn_northbound import impl_idl as idl_ovn

from networking_ovn.octavia import ovn_driver
from networking_ovn.tests.functional import base

LR_REF_KEY_HEADER = 'neutron-'


class TestOctaviaOvnProviderDriver(base.TestOVNFunctionalBase):

@@ -33,6 +37,7 @@ class TestOctaviaOvnProviderDriver(base.TestOVNFunctionalBase):
# ovn_driver.OvnProviderHelper.ovn_nbdb_api is a class variable.
# Set it to None, so that when a worker starts the 2nd test we don't
# use the old object.
idl_ovn.OvnNbApiIdlImpl.ovsdb_connection = None
ovn_driver.OvnProviderHelper.ovn_nbdb_api = None
self.ovn_driver = ovn_driver.OvnProviderDriver()
self.ovn_driver._ovn_helper._octavia_driver_lib = mock.MagicMock()
@@ -48,7 +53,8 @@ class TestOctaviaOvnProviderDriver(base.TestOVNFunctionalBase):
delete_port.return_value = True
self._local_net_cache = {}
self._local_port_cache = {'ports': []}
self.lb_count = None # useful for Multiple LB scenarios
self.addCleanup(self.ovn_driver._ovn_helper.shutdown)
self.core_plugin = directory.get_plugin()

def _mock_get_subnet(self, subnet_id):
m_subnet = mock.MagicMock()
@@ -58,6 +64,22 @@ class TestOctaviaOvnProviderDriver(base.TestOVNFunctionalBase):
def _mock_list_ports(self, **kwargs):
return self._local_port_cache

def _create_provider_network(self):
e1 = self._make_network(self.fmt, 'e1', True,
arg_list=('router:external',
'provider:network_type',
'provider:physical_network'),
**{'router:external': True,
'provider:network_type': 'flat',
'provider:physical_network': 'public'})
res = self._create_subnet(self.fmt, e1['network']['id'],
'100.0.0.0/24', gateway_ip='100.0.0.254',
allocation_pools=[{'start': '100.0.0.2',
'end': '100.0.0.253'}],
enable_dhcp=False)
e1_s1 = self.deserialize(self.fmt, res)
return e1, e1_s1

def _create_lb_model(self, vip=None, vip_network_id=None,
admin_state_up=True):
lb = octavia_data_model.LoadBalancer()
@@ -150,12 +172,19 @@ class TestOctaviaOvnProviderDriver(base.TestOVNFunctionalBase):
self.assertItemsEqual(expected_lbs, observed_lbs)

def _is_lb_associated_to_ls(self, lb_name, ls_name):
return self._is_lb_associated_to_tab(
'Logical_Switch', lb_name, ls_name)

def _is_lb_associated_to_lr(self, lb_name, lr_name):
return self._is_lb_associated_to_tab(
'Logical_Router', lb_name, lr_name)

def _is_lb_associated_to_tab(self, table, lb_name, ls_name):
lb_uuid = self._get_loadbalancer_id(lb_name)
for ls in self.nb_api.tables['Logical_Switch'].rows.values():
for ls in self.nb_api.tables[table].rows.values():
if ls.name == ls_name:
ls_lbs = [lb.uuid for lb in ls.load_balancer]
return lb_uuid in ls_lbs

return False

def _create_router(self, name, gw_info=None):
@@ -185,8 +214,8 @@ class TestOctaviaOvnProviderDriver(base.TestOVNFunctionalBase):
port['port']['id'])

def _update_ls_refs(self, lb_data, net_id, add_ref=True):
if not net_id.startswith("neutron-"):
net_id = "neutron-" + net_id
if not net_id.startswith(LR_REF_KEY_HEADER):
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]:
@@ -202,8 +231,7 @@ class TestOctaviaOvnProviderDriver(base.TestOVNFunctionalBase):
else:
del lb_data[ovn_driver.LB_EXT_IDS_LS_REFS_KEY][net_id]

def _wait_for_status_and_validate(self, lb_data, expected_status,
check_call=True):
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]
update_loadbalancer_status = (
@@ -214,18 +242,26 @@ class TestOctaviaOvnProviderDriver(base.TestOVNFunctionalBase):
if check_call:
self._o_driver_lib.update_loadbalancer_status.assert_has_calls(
expected_calls, any_order=True)

def _wait_for_status_and_validate(self, lb_data, expected_status,
check_call=True):
self._wait_for_status(expected_status, check_call)
expected_lbs = self._make_expected_lbs(lb_data)
self._validate_loadbalancers(expected_lbs)

def _create_load_balancer_and_validate(self, lb_info,
admin_state_up=True,
only_model=False):
only_model=False,
create_router=True,
multiple_lb=False):
self._o_driver_lib.update_loadbalancer_status.reset_mock()
lb_data = {}
router_id = self._create_router("r1")
lb_data[ovn_driver.LB_EXT_IDS_LR_REF_KEY] = router_id
r_id = self._create_router("r1") if create_router else None
if r_id:
lb_data[
ovn_driver.LB_EXT_IDS_LR_REF_KEY] = LR_REF_KEY_HEADER + r_id
net_info = self._create_net(lb_info['vip_network'], lb_info['cidr'],
router_id=router_id)
router_id=r_id)
lb_data['vip_net_info'] = net_info
lb_data['model'] = self._create_lb_model(vip=net_info[2],
vip_network_id=net_info[0],
@@ -250,10 +286,16 @@ class TestOctaviaOvnProviderDriver(base.TestOVNFunctionalBase):
"provisioning_status": "ACTIVE",
"operating_status": o_constants.OFFLINE}]
}
self._wait_for_status_and_validate(lb_data, [expected_status])
if not multiple_lb:
self._wait_for_status_and_validate(lb_data, [expected_status])
else:
l_id = lb_data['model'].loadbalancer_id
self._wait_for_status([expected_status])
self.assertIn(l_id,
[lb['name'] for lb in self._get_loadbalancers()])
self.assertTrue(
self._is_lb_associated_to_ls(lb_data['model'].loadbalancer_id,
'neutron-' + net_info[0]))
LR_REF_KEY_HEADER + net_info[0]))
return lb_data

def _update_load_balancer_and_validate(self, lb_data,
@@ -279,7 +321,8 @@ class TestOctaviaOvnProviderDriver(base.TestOVNFunctionalBase):

self._wait_for_status_and_validate(lb_data, [expected_status])

def _delete_load_balancer_and_validate(self, lb_data, cascade=False):
def _delete_load_balancer_and_validate(self, lb_data, cascade=False,
multiple_lb=False):
self._o_driver_lib.update_loadbalancer_status.reset_mock()
self.ovn_driver.loadbalancer_delete(lb_data['model'], cascade)
expected_status = {
@@ -306,14 +349,19 @@ class TestOctaviaOvnProviderDriver(base.TestOVNFunctionalBase):
"operating_status": "OFFLINE"})
expected_status = {
key: value for key, value in expected_status.items() if value}
l_id = lb_data['model'].loadbalancer_id
lb = lb_data['model']
del lb_data['model']
self._wait_for_status_and_validate(lb_data, [expected_status])
if not multiple_lb:
self._wait_for_status_and_validate(lb_data, [expected_status])
else:
self._wait_for_status([expected_status])
self.assertNotIn(
l_id, [lbs['name'] for lbs in self._get_loadbalancers()])
vip_net_id = lb_data['vip_net_info'][0]
self.assertFalse(
self._is_lb_associated_to_ls(lb.loadbalancer_id,
'neutron-' + vip_net_id))
LR_REF_KEY_HEADER + vip_net_id))

def _make_expected_lbs(self, lb_data):
if not lb_data or not lb_data.get('model'):
@@ -349,7 +397,7 @@ class TestOctaviaOvnProviderDriver(base.TestOVNFunctionalBase):

if lb_data.get(ovn_driver.LB_EXT_IDS_LR_REF_KEY):
external_ids[
ovn_driver.LB_EXT_IDS_LR_REF_KEY] = 'neutron-' + lb_data[
ovn_driver.LB_EXT_IDS_LR_REF_KEY] = lb_data[
ovn_driver.LB_EXT_IDS_LR_REF_KEY]
expected_vips = {}
expected_protocol = ['tcp']
@@ -657,7 +705,6 @@ class TestOctaviaOvnProviderDriver(base.TestOVNFunctionalBase):
if not admin_state_up:
operating_status = 'OFFLINE'
m_listener.protocol = protocol

self.ovn_driver.listener_update(m_listener, m_listener)
pool_status = [{'id': m_listener.default_pool_id,
'provisioning_status': 'ACTIVE'}]
@@ -871,6 +918,121 @@ class TestOctaviaOvnProviderDriver(base.TestOVNFunctionalBase):
lb_data, protocol_port=80, protocol='UDP')
self._delete_load_balancer_and_validate(lb_data)

def _test_lrp_event_handler(self, cascade=False):
# Create Network N1 on router R1 and LBA on N1
lba_data = self._create_load_balancer_and_validate(
{'vip_network': 'N1',
'cidr': '10.0.0.0/24'})
router_id = lba_data[ovn_driver.LB_EXT_IDS_LR_REF_KEY][
len(LR_REF_KEY_HEADER):]
# Create Network N2, connect it to R1
nw_info = self._create_net("N2", "10.0.1.0/24", router_id)

# Check if LBA exists in N2 LS
n_utils.wait_until_true(
lambda: self._is_lb_associated_to_ls(
lba_data['model'].loadbalancer_id,
LR_REF_KEY_HEADER + nw_info[0]),
timeout=10)

# Create Network N3
lbb_data = self._create_load_balancer_and_validate(
{'vip_network': 'N3',
'cidr': '10.0.2.0/24'}, create_router=False, multiple_lb=True)
# Add N3 to R1
self.l3_plugin.add_router_interface(
self.context, lba_data[
ovn_driver.LB_EXT_IDS_LR_REF_KEY][len(LR_REF_KEY_HEADER):],
{'subnet_id': lbb_data['vip_net_info'][1]})

# Check LBB exists on R1
n_utils.wait_until_true(
lambda: self._is_lb_associated_to_lr(
lbb_data['model'].loadbalancer_id,
lba_data[ovn_driver.LB_EXT_IDS_LR_REF_KEY]),
timeout=10)
# Check LBA connected to N3
n_utils.wait_until_true(
lambda: self._is_lb_associated_to_ls(
lba_data['model'].loadbalancer_id,
LR_REF_KEY_HEADER + lbb_data['vip_net_info'][0]),
timeout=10)
# Check LBB connected to N1
n_utils.wait_until_true(
lambda: self._is_lb_associated_to_ls(
lbb_data['model'].loadbalancer_id,
LR_REF_KEY_HEADER + lba_data['vip_net_info'][0]),
timeout=10)
# Check LBB connected to N2
n_utils.wait_until_true(
lambda: self._is_lb_associated_to_ls(
lbb_data['model'].loadbalancer_id,
LR_REF_KEY_HEADER + nw_info[0]),
timeout=10)

lbb_id = lbb_data['model'].loadbalancer_id
if not cascade:
# N3 removed from R1
self.l3_plugin.remove_router_interface(
self.context, lba_data[
ovn_driver.LB_EXT_IDS_LR_REF_KEY][len(LR_REF_KEY_HEADER):],
{'subnet_id': lbb_data['vip_net_info'][1]})
else:
# Delete LBB Cascade
self._delete_load_balancer_and_validate(lbb_data, cascade=True,
multiple_lb=True)

# Check LBB doesn't exists on R1
n_utils.wait_until_true(
lambda: not self._is_lb_associated_to_lr(
lbb_id, lba_data[ovn_driver.LB_EXT_IDS_LR_REF_KEY]),
timeout=10)
# Check LBB not connected to N1
n_utils.wait_until_true(
lambda: not self._is_lb_associated_to_ls(
lbb_id, LR_REF_KEY_HEADER + lba_data['vip_net_info'][0]),
timeout=10)
# Check LBB not connected to N2
n_utils.wait_until_true(
lambda: not self._is_lb_associated_to_ls(
lbb_id, LR_REF_KEY_HEADER + nw_info[0]),
timeout=10)

def test_lrp_event_handler_with_interface_delete(self):
self._test_lrp_event_handler()

def test_lrp_event_handler_with_loadbalancer_cascade_delete(self):
self._test_lrp_event_handler(cascade=True)

def test_lrp_event_handler_lrp_with_external_gateway(self):
# Create Network N1 on router R1 and LBA on N1
lba_data = self._create_load_balancer_and_validate(
{'vip_network': 'N1',
'cidr': '10.0.0.0/24'})
router_id = lba_data[ovn_driver.LB_EXT_IDS_LR_REF_KEY][
len(LR_REF_KEY_HEADER):]

# Create provider network N2, connect it to R1
provider_net, provider_subnet = self._create_provider_network()
self.l3_plugin.update_router(
self.context,
router_id,
{'router': {
'id': router_id,
'external_gateway_info': {
'enable_snat': True,
'network_id': provider_net['network']['id'],
'external_fixed_ips': [
{'ip_address': '100.0.0.2',
'subnet_id': provider_subnet['subnet']['id']}]}}})

# Check if LBA doesn't exist in provider network LS
n_utils.wait_until_true(
lambda: not self._is_lb_associated_to_ls(
lba_data['model'].loadbalancer_id,
LR_REF_KEY_HEADER + provider_net['network']['id']),
timeout=10)

def test_lb_listener_pool_workflow(self):
lb_data = self._create_load_balancer_and_validate(
{'vip_network': 'vip_network',


+ 11
- 4
networking_ovn/tests/unit/l3/test_l3_ovn.py View File

@@ -69,7 +69,9 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
'networks': ['10.0.0.100/24'],
'external_ids': {
ovn_const.OVN_SUBNET_EXT_IDS_KEY: 'subnet-id',
ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}}
ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1',
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY:
utils.ovn_name(self.fake_network['id'])}}
self.fake_router_ports = [self.fake_router_port]
self.fake_subnet = {'id': 'subnet-id',
'ip_version': 4,
@@ -123,7 +125,9 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
'may_exist': True,
'external_ids': {
ovn_const.OVN_SUBNET_EXT_IDS_KEY: 'ext-subnet-id',
ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'},
ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1',
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY:
utils.ovn_name(self.fake_network['id'])},
'gateway_chassis': ['hv1']}
self.fake_floating_ip_attrs = {'floating_ip_address': '192.168.0.10',
'fixed_ip_address': '10.0.0.10'}
@@ -246,7 +250,8 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
'fixed_ips': [
{'ip_address': '2001:db8::1', 'subnet_id': 'subnet-id1'},
{'ip_address': '2001:dba::1', 'subnet_id': 'subnet-id2'}],
'mac_address': 'aa:aa:aa:aa:aa:aa'
'mac_address': 'aa:aa:aa:aa:aa:aa',
'network_id': 'network-id1'
}
fake_rtr_intf_networks = ['2001:db8::1/24', '2001:dba::1/24']
self.l3_inst.add_router_interface(self.context, router_id,
@@ -288,7 +293,9 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
networks=['10.0.0.100/24'],
external_ids={
ovn_const.OVN_SUBNET_EXT_IDS_KEY: 'subnet-id',
ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'})
ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1',
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY:
utils.ovn_name(self.fake_network['id'])})

@mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.'
'update_router')


+ 515
- 26
networking_ovn/tests/unit/octavia/test_ovn_driver.py View File

@@ -11,18 +11,20 @@
# License for the specific language governing permissions and limitations
# under the License.
#
import collections

import mock
from neutron.tests import base
from octavia_lib.api.drivers import data_models
from octavia_lib.api.drivers import exceptions
from octavia_lib.common import constants
from oslo_utils import uuidutils
from ovsdbapp.backend.ovs_idl import idlutils

from networking_ovn.common import constants as ovn_const
from networking_ovn.octavia import ovn_driver
from networking_ovn.tests.unit import fakes


# TODO(mjozefcz): Move it to unittest fakes.
class MockedLB(data_models.LoadBalancer):
def __init__(self, *args, **kwargs):
self.external_ids = kwargs.pop('ext_ids')
@@ -50,6 +52,8 @@ class TestOvnOctaviaBase(base.BaseTestCase):
self.vip_port_id = uuidutils.generate_uuid()
self.vip_subnet_id = uuidutils.generate_uuid()
mock.patch("networking_ovn.common.utils.get_ovsdb_connection").start()
mock.patch(
"networking_ovn.octavia.ovn_driver.OvnNbIdlForLb").start()
self.member_address = "192.168.2.149"
self.vip_address = '192.148.210.109'
self.member_port = "1010"
@@ -439,7 +443,8 @@ class TestOvnProviderHelper(TestOvnOctaviaBase):
'subnet_id': self.member_subnet_id,
'pool_id': self.member_pool_id,
'admin_state_up': True}
mock.patch.object(self.helper, 'ovn_nbdb_api').start()
self.ovn_nbdb_api = mock.patch.object(self.helper, 'ovn_nbdb_api')
self.ovn_nbdb_api.start()
add_req_thread = mock.patch.object(ovn_driver.OvnProviderHelper,
'add_request')
self.mock_add_request = add_req_thread.start()
@@ -452,7 +457,27 @@ class TestOvnProviderHelper(TestOvnOctaviaBase):
execute.return_value = [self.ovn_lb]
self.helper.ovn_nbdb_api.db_list_rows.return_value.\
execute.return_value = [self.ovn_lb]
ref_lb1 = MockedLB(
mock.patch.object(self.helper,
'_find_ovn_lb_with_pool_key',
return_value=self.ovn_lb).start()
mock.patch.object(self.helper,
'_get_pool_listeners',
return_value=[]).start()
self._update_lb_to_ls_association = mock.patch.object(
self.helper,
'_update_lb_to_ls_association',
return_value=[])
self._update_lb_to_ls_association.start()
self._update_lb_to_lr_association = mock.patch.object(
self.helper,
'_update_lb_to_lr_association',
return_value=[])
self._update_lb_to_lr_association.start()

# NOTE(mjozefcz): Create foo router and network.
net_id = uuidutils.generate_uuid()
router_id = uuidutils.generate_uuid()
self.ref_lb1 = MockedLB(
uuid=uuidutils.generate_uuid(),
admin_state_up=True,
listeners=[],
@@ -461,8 +486,11 @@ class TestOvnProviderHelper(TestOvnOctaviaBase):
project_id=self.project_id,
vip_address=self.vip_address,
vip_network_id=self.vip_network_id,
ext_ids={ovn_driver.LB_EXT_IDS_LR_REF_KEY: '1'})
ref_lb2 = MockedLB(
ext_ids={
ovn_driver.LB_EXT_IDS_LR_REF_KEY: "neutron-%s" % net_id,
ovn_driver.LB_EXT_IDS_LS_REFS_KEY:
'{\"neutron-%s\": 1}' % net_id})
self.ref_lb2 = MockedLB(
uuid=uuidutils.generate_uuid(),
admin_state_up=True,
listeners=[],
@@ -471,25 +499,24 @@ class TestOvnProviderHelper(TestOvnOctaviaBase):
project_id=self.project_id,
vip_address=self.vip_address,
vip_network_id=self.vip_network_id,
ext_ids={ovn_driver.LB_EXT_IDS_LR_REF_KEY: '1'})
loadbalancer = collections.namedtuple("LoadBalancer",
"load_balancer name uuid")
mock.patch.object(self.helper,
'_find_ovn_lb_with_pool_key',
return_value=self.ovn_lb).start()
mock.patch.object(self.helper,
'_get_pool_listeners',
return_value=[]).start()
mock.patch.object(self.helper,
'_update_lb_to_ls_association',
return_value=[]).start()
mock.patch.object(self.helper,
'_update_lb_to_lr_association',
return_value=[]).start()
self.router = loadbalancer(load_balancer=[ref_lb1], name='router_lb',
uuid=uuidutils.generate_uuid())
self.network = loadbalancer(load_balancer=[ref_lb2], name='network_lb',
uuid=uuidutils.generate_uuid())
ext_ids={
ovn_driver.LB_EXT_IDS_LR_REF_KEY: "neutron-%s" % net_id,
ovn_driver.LB_EXT_IDS_LS_REFS_KEY:
'{\"neutron-%s\": 1}' % net_id})
# TODO(mjozefcz): Consider using FakeOVNRouter.
self.router = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'load_balancer': [self.ref_lb1],
'name': 'neutron-%s' % router_id,
'ports': []})
# TODO(mjozefcz): Consider using FakeOVNSwitch.
self.network = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'load_balancer': [self.ref_lb2],
'name': 'neutron-%s' % net_id,
'uuid': net_id})
self.mock_get_nw = mock.patch.object(
self.helper, "_get_nw_router_info_on_interface_event",
return_value=(self.router, self.network))
self.mock_get_nw.start()

@mock.patch('networking_ovn.octavia.ovn_driver.get_network_driver')
def test_lb_create(self, net_dr):
@@ -624,7 +651,7 @@ class TestOvnProviderHelper(TestOvnOctaviaBase):
self.assertEqual(status['pools'][0]['provisioning_status'],
constants.ACTIVE)
self.assertEqual(status['pools'][0]['operating_status'],
constants.ONLINE)
constants.OFFLINE)

def test_pool_delete(self):
status = self.helper.pool_delete(self.pool)
@@ -684,3 +711,465 @@ class TestOvnProviderHelper(TestOvnOctaviaBase):
constants.ACTIVE)
self.assertEqual(status['members'][0]['provisioning_status'],
constants.DELETED)

@mock.patch('networking_ovn.octavia.ovn_driver.get_network_driver')
def test_logical_router_port_event_create(self, net_dr):
self.router_port_event = ovn_driver.LogicalRouterPortEvent(
self.helper)
row = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'gateway_chassis': []})
self.router_port_event.run('create', row, mock.ANY)
expected = {
'info':
{'router': self.router,
'network': self.network},
'type': 'lb_create_lrp_assoc'}
self.mock_add_request.assert_called_once_with(expected)

@mock.patch('networking_ovn.octavia.ovn_driver.get_network_driver')
def test_logical_router_port_event_delete(self, net_dr):
self.router_port_event = ovn_driver.LogicalRouterPortEvent(
self.helper)
row = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'gateway_chassis': []})
self.router_port_event.run('delete', row, mock.ANY)
expected = {
'info':
{'router': self.router,
'network': self.network},
'type': 'lb_delete_lrp_assoc'}
self.mock_add_request.assert_called_once_with(expected)

@mock.patch('networking_ovn.octavia.ovn_driver.get_network_driver')
def test_logical_router_port_event_gw_port(self, net_dr):
self.router_port_event = ovn_driver.LogicalRouterPortEvent(
self.helper)
row = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'gateway_chassis': ['temp-gateway-chassis']})
self.router_port_event.run(mock.ANY, row, mock.ANY)
self.mock_add_request.assert_not_called()

def test__get_nw_router_info_on_interface_event(self):
self.mock_get_nw.stop()
lrp = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={
'external_ids': {
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'router1',
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: 'network1'}
})
self.helper._get_nw_router_info_on_interface_event(lrp)
expected_calls = [
mock.call.lookup('Logical_Router', 'neutron-router1'),
mock.call.lookup('Logical_Switch', 'network1')]
self.helper.ovn_nbdb_api.assert_has_calls(expected_calls)

def test__get_nw_router_info_on_interface_event_not_found(self):
self.mock_get_nw.stop()
self.helper.ovn_nbdb_api.lookup.side_effect = [idlutils.RowNotFound]
lrp = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={
'external_ids': {
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'router1'}
})
self.assertRaises(
idlutils.RowNotFound,
self.helper._get_nw_router_info_on_interface_event,
lrp)

def test_lb_delete_lrp_assoc_handler(self):
lrp = fakes.FakeOvsdbRow.create_one_ovsdb_row()
self.helper.lb_delete_lrp_assoc_handler(lrp)
expected = {
'info':
{'router': self.router,
'network': self.network},
'type': 'lb_delete_lrp_assoc'}
self.mock_add_request.assert_called_once_with(expected)

def test_lb_delete_lrp_assoc_handler_info_not_found(self):
self.mock_get_nw.stop()
self.helper.ovn_nbdb_api.lookup.side_effect = [idlutils.RowNotFound]
lrp = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={
'external_ids': {
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'router1'}
})
self.helper.lb_delete_lrp_assoc_handler(lrp)
self.mock_add_request.assert_not_called()

@mock.patch.object(ovn_driver.OvnProviderHelper,
'_execute_commands')
def test_lb_delete_lrp_assoc_no_net_lb_no_r_lb(self, mock_execute):
info = {
'network': self.network,
'router': self.router,
}
self.network.load_balancer = []
self.router.load_balancer = []
self.helper.lb_delete_lrp_assoc(info)
self.helper._update_lb_to_lr_association.assert_not_called()
mock_execute.assert_not_called()

@mock.patch.object(ovn_driver.OvnProviderHelper,
'_execute_commands')
def test_lb_delete_lrp_assoc_no_net_lb_r_lb(self, mock_execute):
info = {
'network': self.network,
'router': self.router,
}
self.network.load_balancer = []
self.helper.lb_delete_lrp_assoc(info)
expected = [
self.helper.ovn_nbdb_api.ls_lb_del(
self.network.uuid,
self.router.load_balancer[0].uuid
),
]
self.helper._update_lb_to_lr_association.assert_not_called()
mock_execute.assert_called_once_with(expected)

@mock.patch.object(ovn_driver.OvnProviderHelper,
'_execute_commands')
def test_lb_delete_lrp_assoc_net_lb_no_r_lb(self, mock_execute):
info = {
'network': self.network,
'router': self.router,
}
self.router.load_balancer = []
self.helper.lb_delete_lrp_assoc(info)
mock_execute.assert_not_called()
self.helper._update_lb_to_lr_association.assert_called_once_with(
self.network.load_balancer[0], self.router, delete=True
)

@mock.patch.object(ovn_driver.OvnProviderHelper,
'_execute_commands')
def test_lb_delete_lrp_assoc(self, mock_execute):
info = {
'network': self.network,
'router': self.router,
}
self.helper.lb_delete_lrp_assoc(info)
self.helper._update_lb_to_lr_association.assert_called_once_with(
self.network.load_balancer[0], self.router, delete=True
)
expected = [
self.helper.ovn_nbdb_api.ls_lb_del(
self.network.uuid,
self.router.load_balancer[0].uuid
),
]
mock_execute.assert_called_once_with(expected)

def test_lb_create_lrp_assoc_handler(self):
lrp = fakes.FakeOvsdbRow.create_one_ovsdb_row()
self.helper.lb_create_lrp_assoc_handler(lrp)
expected = {
'info':
{'router': self.router,
'network': self.network},
'type': 'lb_create_lrp_assoc'}
self.mock_add_request.assert_called_once_with(expected)

@mock.patch.object(ovn_driver.OvnProviderHelper,
'_execute_commands')
def test_lb_create_lrp_assoc(self, mock_execute):
info = {
'network': self.network,
'router': self.router,
}
self.helper.lb_create_lrp_assoc(info)
self.helper._update_lb_to_lr_association.assert_called_once_with(
self.network.load_balancer[0], self.router
)
expected = [
self.helper.ovn_nbdb_api.ls_lb_add(
self.network.uuid,
self.router.load_balancer[0].uuid
),
]
mock_execute.assert_called_once_with(expected)

@mock.patch.object(ovn_driver.OvnProviderHelper,
'_execute_commands')
def test_lb_create_lrp_assoc_uniq_lb(self, mock_execute):
info = {
'network': self.network,
'router': self.router,
}
# Make it already uniq.
self.network.load_balancer = self.router.load_balancer
self.helper.lb_create_lrp_assoc(info)
self.helper._update_lb_to_lr_association.assert_not_called()
mock_execute.assert_not_called()

def test__find_lb_in_ls(self):
net_lb = self.helper._find_lb_in_ls(self.network)
for lb in self.network.load_balancer:
self.assertIn(lb, net_lb)

def test__find_lb_in_ls_wrong_ref(self):
# lets break external_ids refs
self.network.load_balancer[0].external_ids.update({
ovn_driver.LB_EXT_IDS_LS_REFS_KEY: 'foo'})
net_lb = self.helper._find_lb_in_ls(self.network)
for lb in self.network.load_balancer:
self.assertNotIn(lb, net_lb)

@mock.patch('networking_ovn.octavia.ovn_driver.get_network_driver')
def test__find_ls_for_lr(self, net_dr):
fake_subnet1 = fakes.FakeSubnet.create_one_subnet()
fake_subnet1.network_id = 'foo1'
fake_subnet2 = fakes.FakeSubnet.create_one_subnet()
fake_subnet2.network_id = 'foo2'
net_dr.return_value.get_subnet.side_effect = [
fake_subnet1, fake_subnet2]
p1 = fakes.FakeOVNPort.create_one_port(attrs={
'gateway_chassis': [],
'external_ids': {
ovn_const.OVN_SUBNET_EXT_IDS_KEY:
'%s %s' % (fake_subnet1.id,
fake_subnet2.id)}})
self.router.ports.append(p1)
res = self.helper._find_ls_for_lr(self.router)
self.assertListEqual(['neutron-foo1', 'neutron-foo2'],
res)

@mock.patch('networking_ovn.octavia.ovn_driver.get_network_driver')
def test__find_ls_for_lr_gw_port(self, net_dr):
p1 = fakes.FakeOVNPort.create_one_port(attrs={
'gateway_chassis': ['foo-gw-chassis'],
'external_ids': {
ovn_const.OVN_SUBNET_EXT_IDS_KEY: self.member_subnet_id}})
self.router.ports.append(p1)
result = self.helper._find_ls_for_lr(self.router)
self.assertListEqual([], result)

@mock.patch.object(
ovn_driver.OvnProviderHelper, '_del_lb_to_lr_association')
@mock.patch.object(
ovn_driver.OvnProviderHelper, '_add_lb_to_lr_association')
def test__update_lb_to_lr_association(self, add, delete):
self._update_lb_to_lr_association.stop()
self.helper._update_lb_to_lr_association(self.ref_lb1, self.router)
lr_ref = self.ref_lb1.external_ids.get(
ovn_driver.LB_EXT_IDS_LR_REF_KEY)
add.assert_called_once_with(self.ref_lb1, self.router, lr_ref)
delete.assert_not_called()

@mock.patch.object(
ovn_driver.OvnProviderHelper, '_del_lb_to_lr_association')
@mock.patch.object(
ovn_driver.OvnProviderHelper, '_add_lb_to_lr_association')
def test__update_lb_to_lr_association_delete(self, add, delete):
self._update_lb_to_lr_association.stop()
self.helper._update_lb_to_lr_association(
self.ref_lb1, self.router, delete=True)
lr_ref = self.ref_lb1.external_ids.get(
ovn_driver.LB_EXT_IDS_LR_REF_KEY)
add.assert_not_called()
delete.assert_called_once_with(self.ref_lb1, self.router, lr_ref)

@mock.patch('networking_ovn.octavia.ovn_driver.get_network_driver')
def test__del_lb_to_lr_association(self, net_dr):
lr_ref = self.ref_lb1.external_ids.get(
ovn_driver.LB_EXT_IDS_LR_REF_KEY)
upd_lr_ref = '%s,%s' % (lr_ref, self.router.name)
self.helper._del_lb_to_lr_association(
self.ref_lb1, self.router, upd_lr_ref)
expected_calls = [
mock.call.db_set(
'Load_Balancer', self.ref_lb1.uuid,
(('external_ids',
{ovn_driver.LB_EXT_IDS_LR_REF_KEY: lr_ref}))),
mock.call.lr_lb_del(
self.router.uuid, self.ref_lb1.uuid,
if_exists=True)]
self.helper.ovn_nbdb_api.assert_has_calls(
expected_calls)
self.helper.ovn_nbdb_api.db_remove.assert_not_called()

@mock.patch('networking_ovn.octavia.ovn_driver.get_network_driver')
def test__del_lb_to_lr_association_no_lr_ref(self, net_dr):
lr_ref = ''
self.helper._del_lb_to_lr_association(
self.ref_lb1, self.router, lr_ref)
self.helper.ovn_nbdb_api.db_set.assert_not_called()
self.helper.ovn_nbdb_api.db_remove.assert_not_called()
self.helper.ovn_nbdb_api.lr_lb_del.assert_not_called()

@mock.patch('networking_ovn.octavia.ovn_driver.get_network_driver')
def test__del_lb_to_lr_association_lr_ref_empty_after(self, net_dr):
lr_ref = self.router.name
self.helper._del_lb_to_lr_association(
self.ref_lb1, self.router, lr_ref)
self.helper.ovn_nbdb_api.db_remove.assert_called_once_with(
'Load_Balancer', self.ref_lb1.uuid, 'external_ids',
ovn_driver.LB_EXT_IDS_LR_REF_KEY)
self.helper.ovn_nbdb_api.lr_lb_del.assert_called_once_with(
self.router.uuid, self.ref_lb1.uuid, if_exists=True)
self.helper.ovn_nbdb_api.db_set.assert_not_called()

@mock.patch.object(ovn_driver.OvnProviderHelper, '_find_ls_for_lr')
def test__del_lb_to_lr_association_from_ls(self, f_ls):
# This test if LB is deleted from Logical_Router_Port
# Logical_Switch.
f_ls.return_value = ['neutron-xyz', 'neutron-qwr']
self.helper._del_lb_to_lr_association(self.ref_lb1, self.router, '')
self.helper.ovn_nbdb_api.ls_lb_del.assert_has_calls([
(mock.call('neutron-xyz', self.ref_lb1.uuid, if_exists=True)),
(mock.call('neutron-qwr', self.ref_lb1.uuid, if_exists=True))])

@mock.patch.object(ovn_driver.OvnProviderHelper, '_find_ls_for_lr')
def test__add_lb_to_lr_association(self, f_ls):
lr_ref = 'foo'
f_ls.return_value = ['neutron-xyz', 'neutron-qwr']
self.helper._add_lb_to_lr_association(
self.ref_lb1, self.router, lr_ref)
self.helper.ovn_nbdb_api.lr_lb_add.assert_called_once_with(
self.router.uuid, self.ref_lb1.uuid, may_exist=True)
self.helper.ovn_nbdb_api.ls_lb_add.assert_has_calls([
(mock.call('neutron-xyz', self.ref_lb1.uuid, may_exist=True)),
(mock.call('neutron-qwr', self.ref_lb1.uuid, may_exist=True))])
self.helper.ovn_nbdb_api.db_set.assert_called_once_with(
'Load_Balancer', self.ref_lb1.uuid,
('external_ids', {'lr_ref': 'foo,%s' % self.router.name}))

def test__find_lr_of_ls(self):
lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={
'external_ids': {
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'router1'},
'type': 'router',
'options': {
'router-port': 'lrp-foo-name'}
})
lrp = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={
'name': 'lrp-foo-name'
})
lr = fakes.FakeOVNRouter.create_one_router(
attrs={
'name': 'router1',
'ports': [lrp]})
ls = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'ports': [lsp]})

(self.helper.ovn_nbdb_api.tables['Logical_Router'].rows.
values.return_value) = [lr]
returned_lr = self.helper._find_lr_of_ls(ls)
self.assertEqual(lr, returned_lr)

def test__find_lr_of_ls_no_lrp(self):
ls = fakes.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'ports': []})
returned_lr = self.helper._find_lr_of_ls(ls)
(self.helper.ovn_nbdb_api.tables['Logical_Router'].rows.
values.assert_not_called())
self.assertIsNone(returned_lr)

def test__update_lb_to_ls_association_empty_network_and_subnet(self):
self._update_lb_to_ls_association.stop()
returned_commands = self.helper._update_lb_to_ls_association(
self.ref_lb1, associate=True)
self.assertListEqual(returned_commands, [])

def test__update_lb_to_ls_association_network(self):
self._update_lb_to_ls_association.stop()

self.helper._update_lb_to_ls_association(
self.ref_lb1, network_id=self.network.uuid, associate=True)

self.helper.ovn_nbdb_api.ls_get.assert_called_once_with(
self.network.name)
ls_refs = {'ls_refs': '{"%s": 2}' % self.network.name}
self.helper.ovn_nbdb_api.db_set.assert_called_once_with(
'Load_Balancer', self.ref_lb1.uuid, ('external_ids', ls_refs))

@mock.patch('networking_ovn.octavia.ovn_driver.get_network_driver')
def test__update_lb_to_ls_association_subnet(self, net_dr):
self._update_lb_to_ls_association.stop()
subnet = fakes.FakeSubnet.create_one_subnet(
attrs={'id': 'foo_subnet_id',
'name': 'foo_subnet_name',
'network_id': 'foo_network_id'})
net_dr.return_value.get_subnet.return_value = subnet

self.helper._update_lb_to_ls_association(
self.ref_lb1, subnet_id=subnet.id, associate=True)

self.helper.ovn_nbdb_api.ls_get.assert_called_once_with(
'neutron-foo_network_id')

def test__update_lb_to_ls_association_empty_ls_refs(self):
self._update_lb_to_ls_association.stop()
(self.helper.ovn_nbdb_api.ls_get.return_value.execute.
return_value) = self.network
self.ref_lb1.external_ids.pop('ls_refs')

self.helper._update_lb_to_ls_association(
self.ref_lb1, network_id=self.network.uuid)

self.helper.ovn_nbdb_api.ls_lb_add.assert_called_once_with(
self.network.uuid, self.ref_lb1.uuid, may_exist=True)
ls_refs = {'ls_refs': '{"%s": 1}' % self.network.name}
self.helper.ovn_nbdb_api.db_set.assert_called_once_with(
'Load_Balancer', self.ref_lb1.uuid, ('external_ids', ls_refs))

@mock.patch('networking_ovn.octavia.ovn_driver.get_network_driver')
def test__update_lb_to_ls_association_no_ls(self, net_dr):
self._update_lb_to_ls_association.stop()
(self.helper.ovn_nbdb_api.ls_get.return_value.execute.
return_value) = None

returned_commands = self.helper._update_lb_to_ls_association(
self.ref_lb1, network_id=self.network.uuid)

self.helper.ovn_nbdb_api.ls_get.assert_called_once_with(
self.network.name)
self.assertListEqual([], returned_commands)

def test__update_lb_to_ls_association_network_disassociate(self):
self._update_lb_to_ls_association.stop()
(self.helper.ovn_nbdb_api.ls_get.return_value.execute.
return_value) = self.network

self.helper._update_lb_to_ls_association(
self.ref_lb1, network_id=self.network.uuid, associate=False)

self.helper.ovn_nbdb_api.ls_get.assert_called_once_with(
self.network.name)
self.helper.ovn_nbdb_api.db_set.assert_called_once_with(
'Load_Balancer', self.ref_lb1.uuid,
('external_ids', {'ls_refs': '{}'}))
self.helper.ovn_nbdb_api.ls_lb_del.assert_called_once_with(
self.network.uuid, self.ref_lb1.uuid, if_exists=True)

def test__update_lb_to_ls_association_disassoc_ls_not_in_ls_refs(self):
self._update_lb_to_ls_association.stop()
(self.helper.ovn_nbdb_api.ls_get.return_value.execute.
return_value) = self.network
self.ref_lb1.external_ids.pop('ls_refs')

self.helper._update_lb_to_ls_association(
self.ref_lb1, network_id=self.network.uuid, associate=False)

self.helper.ovn_nbdb_api.ls_lb_del.assert_not_called()
self.helper.ovn_nbdb_api.db_set.assert_not_called()

def test__update_lb_to_ls_association_disassoc_multiple_refs(self):
self._update_lb_to_ls_association.stop()
(self.helper.ovn_nbdb_api.ls_get.return_value.execute.
return_value) = self.network
# multiple refs
ls_refs = {'ls_refs': '{"%s": 2}' % self.network.name}
self.ref_lb1.external_ids.update(ls_refs)

self.helper._update_lb_to_ls_association(
self.ref_lb1, network_id=self.network.uuid, associate=False)

self.helper.ovn_nbdb_api.ls_get.assert_called_once_with(
self.network.name)
exp_ls_refs = {'ls_refs': '{"%s": 1}' % self.network.name}
self.helper.ovn_nbdb_api.db_set.assert_called_once_with(
'Load_Balancer', self.ref_lb1.uuid, ('external_ids', exp_ls_refs))

Loading…
Cancel
Save