Browse Source

Merge "[OVN] Add support for router availability zones"

changes/74/738274/1
Zuul 2 weeks ago
committed by Gerrit Code Review
parent
commit
5787708a01
13 changed files with 354 additions and 26 deletions
  1. +6
    -0
      neutron/common/ovn/constants.py
  2. +8
    -1
      neutron/common/ovn/extensions.py
  3. +31
    -3
      neutron/common/ovn/utils.py
  4. +33
    -1
      neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py
  5. +25
    -10
      neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py
  6. +5
    -2
      neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py
  7. +45
    -2
      neutron/services/ovn_l3/plugin.py
  8. +28
    -0
      neutron/tests/unit/common/ovn/test_utils.py
  9. +32
    -0
      neutron/tests/unit/fake_resources.py
  10. +3
    -3
      neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovn_db_sync.py
  11. +72
    -0
      neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py
  12. +60
    -4
      neutron/tests/unit/services/ovn_l3/test_plugin.py
  13. +6
    -0
      releasenotes/notes/ovn-router-availability-zones-03a802ee19689474.yaml

+ 6
- 0
neutron/common/ovn/constants.py View File

@@ -25,6 +25,7 @@ OVN_NETWORK_MTU_EXT_ID_KEY = 'neutron:mtu'
OVN_PORT_NAME_EXT_ID_KEY = 'neutron:port_name'
OVN_PORT_FIP_EXT_ID_KEY = 'neutron:port_fip'
OVN_ROUTER_NAME_EXT_ID_KEY = 'neutron:router_name'
OVN_ROUTER_AZ_HINTS_EXT_ID_KEY = 'neutron:availability_zone_hints'
OVN_ROUTER_IS_EXT_GW = 'neutron:is_ext_gw'
OVN_GW_PORT_EXT_ID_KEY = 'neutron:gw_port_id'
OVN_SUBNET_EXT_ID_KEY = 'neutron:subnet_id'
@@ -287,3 +288,8 @@ MCAST_FLOOD_UNREGISTERED = 'mcast_flood_unregistered'
EXTERNAL_PORT_TYPES = (portbindings.VNIC_DIRECT,
portbindings.VNIC_DIRECT_PHYSICAL,
portbindings.VNIC_MACVTAP)

NEUTRON_AVAILABILITY_ZONES = 'neutron-availability-zones'
OVN_CMS_OPTIONS = 'ovn-cms-options'
CMS_OPT_CHASSIS_AS_GW = 'enable-chassis-as-gw'
CMS_OPT_AVAILABILITY_ZONES = 'availability-zones'

+ 8
- 1
neutron/common/ovn/extensions.py View File

@@ -11,6 +11,11 @@
# License for the specific language governing permissions and limitations
# under the License.


from neutron_lib.api.definitions import agent as agent_def
from neutron_lib.api.definitions import availability_zone as az_def
from neutron_lib.api.definitions import router_availability_zone as raz_def

# NOTE(russellb) This remains in its own file (vs constants.py) because we want
# to be able to easily import it and export the info without any dependencies
# on external imports.
@@ -26,13 +31,15 @@ ML2_SUPPORTED_API_EXTENSIONS_OVN_L3 = [
'sorting',
'project-id',
'dns-integration',
agent_def.ALIAS,
az_def.ALIAS,
raz_def.ALIAS,
]
ML2_SUPPORTED_API_EXTENSIONS = [
'address-scope',
'agent',
'allowed-address-pairs',
'auto-allocated-topology',
'availability_zone',
'binding',
'default-subnetpools',
'external-net',


+ 31
- 3
neutron/common/ovn/utils.py View File

@@ -16,6 +16,7 @@ import os
import re

import netaddr
from neutron_lib.api.definitions import availability_zone as az_def
from neutron_lib.api.definitions import external_net
from neutron_lib.api.definitions import extra_dhcp_opt as edo_ext
from neutron_lib.api.definitions import l3
@@ -27,6 +28,7 @@ from neutron_lib import context as n_context
from neutron_lib import exceptions as n_exc
from neutron_lib.plugins import directory
from neutron_lib.utils import net as n_utils
from oslo_config import cfg
from oslo_log import log
from oslo_utils import netutils
from oslo_utils import strutils
@@ -40,6 +42,7 @@ from neutron.common.ovn import exceptions as ovn_exc

LOG = log.getLogger(__name__)

CONF = cfg.CONF

DNS_RESOLVER_FILE = "/etc/resolv.conf"

@@ -489,11 +492,15 @@ def compute_address_pairs_diff(ovn_port, neutron_port):
return AddrPairsDiff(added, removed, changed=any(added or removed))


def get_ovn_cms_options(chassis):
"""Return the list of CMS options in a Chassis."""
return [opt.strip() for opt in chassis.external_ids.get(
constants.OVN_CMS_OPTIONS, '').split(',')]


def is_gateway_chassis(chassis):
"""Check if the given chassis is a gateway chassis"""
external_ids = getattr(chassis, 'external_ids', {})
return ('enable-chassis-as-gw' in external_ids.get(
'ovn-cms-options', '').split(','))
return constants.CMS_OPT_CHASSIS_AS_GW in get_ovn_cms_options(chassis)


def get_port_capabilities(port):
@@ -513,3 +520,24 @@ def get_port_id_from_gwc_row(row):
:returns: String containing router port_id.
"""
return constants.RE_PORT_FROM_GWC.search(row.name).group(2)


def get_az_hints(resource):
"""Return the availability zone hints from a given resource."""
return (resource.get(az_def.AZ_HINTS) or CONF.default_availability_zones)


def get_chassis_availability_zones(chassis):
"""Return a list of availability zones from a given OVN Chassis."""
azs = []
if not chassis:
return azs

opt_key = constants.CMS_OPT_AVAILABILITY_ZONES + '='
for opt in get_ovn_cms_options(chassis):
if not opt.startswith(opt_key):
continue
values = opt.split('=')[1]
azs = [az.strip() for az in values.split(':') if az.strip()]
break
return azs

+ 33
- 1
neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py View File

@@ -270,6 +270,10 @@ class OVNMechanismDriver(api.MechanismDriver):
self.patch_plugin_choose("update_agent", update_agent)
self.patch_plugin_choose("delete_agent", delete_agent)

# Override availability zone methods
self.patch_plugin_merge("get_availability_zones",
get_availability_zones)

# Now IDL connections can be safely used.
self._post_fork_event.set()

@@ -1091,7 +1095,8 @@ class OVNMechanismDriver(api.MechanismDriver):
'binary': binary,
'host': chassis.hostname,
'heartbeat_timestamp': timeutils.utcnow(),
'availability_zone': 'n/a',
'availability_zone': ', '.join(
ovn_utils.get_chassis_availability_zones(chassis)),
'topic': 'n/a',
'description': description,
'configurations': {
@@ -1180,6 +1185,27 @@ class OVNMechanismDriver(api.MechanismDriver):
txn.add(self._nb_ovn.check_liveness())
return True

def list_availability_zones(self, context, filters=None):
"""List all availability zones from gateway chassis."""
azs = {}
# TODO(lucasagomes): In the future, once the agents API in OVN
# gets more stable we should consider getting the information from
# the availability zones from the agents API itself. That would
# allow us to do things like: Do not schedule router ports on
# chassis that are offline (via the "alive" attribute for agents).
for ch in self._sb_ovn.chassis_list().execute(check_error=True):
# Only take in consideration gateway chassis because that's where
# the router ports are scheduled on
if not ovn_utils.is_gateway_chassis(ch):
continue

azones = ovn_utils.get_chassis_availability_zones(ch)
for azone in azones:
azs[azone] = {'name': azone, 'resource': 'router',
'state': 'available',
'tenant_id': context.project_id}
return azs


def populate_agents(driver):
for ch in driver._sb_ovn.tables['Chassis'].rows.values():
@@ -1266,3 +1292,9 @@ def create_default_drop_port_group(nb_idl):
if ports_with_pg:
# Add the ports to the default Port Group
txn.add(nb_idl.pg_add_ports(pg_name, list(ports_with_pg)))


def get_availability_zones(cls, context, _driver, filters=None, fields=None,
sorts=None, limit=None, marker=None,
page_reverse=False):
return list(_driver.list_availability_zones(context, filters).values())

+ 25
- 10
neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py View File

@@ -1024,12 +1024,11 @@ class OVNClient(object):

def _add_router_ext_gw(self, router, networks, txn):
context = n_context.get_admin_context()
router_id = router['id']
# 1. Add the external gateway router port.
gateways = self._get_gw_info(context, router)
gw_port_id = router['gw_port_id']
port = self._plugin.get_port(context, gw_port_id)
self._create_lrouter_port(context, router_id, port, txn=txn)
self._create_lrouter_port(context, router, port, txn=txn)

def _build_extids(gw_info):
# TODO(lucasagomes): Remove this check after OVS 2.8.2 is tagged
@@ -1044,7 +1043,7 @@ class OVNClient(object):
return columns

# 2. Add default route with nexthop as gateway ip
lrouter_name = utils.ovn_name(router_id)
lrouter_name = utils.ovn_name(router['id'])
for gw_info in gateways:
columns = _build_extids(gw_info)
txn.add(self._nb_idl.add_static_route(
@@ -1138,7 +1137,9 @@ class OVNClient(object):
ovn_const.OVN_GW_PORT_EXT_ID_KEY:
router.get('gw_port_id') or '',
ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(utils.get_revision_number(
router, ovn_const.TYPE_ROUTERS))}
router, ovn_const.TYPE_ROUTERS)),
ovn_const.OVN_ROUTER_AZ_HINTS_EXT_ID_KEY:
','.join(utils.get_az_hints(router))}

def create_router(self, context, router, add_external_gateway=True):
"""Create a logical router."""
@@ -1259,13 +1260,16 @@ class OVNClient(object):
db_rev.delete_revision(context, router_id, ovn_const.TYPE_ROUTERS)

def get_candidates_for_scheduling(self, physnet, cms=None,
chassis_physnets=None):
chassis_physnets=None,
availability_zone_hints=None):
"""Return chassis for scheduling gateway router.

Criteria for selecting chassis as candidates
1) chassis from cms with proper bridge mappings
2) if no chassis is available from 1) then,
select chassis with proper bridge mappings
3) Filter the available chassis accordingly to the routers
availability zone hints (if present)
"""
# TODO(lucasagomes): Simplify the logic here, the CMS option has
# been introduced long ago and by now all gateway chassis should
@@ -1283,6 +1287,16 @@ class OVNClient(object):
else:
bmaps.append(chassis)
candidates = cms_bmaps or bmaps

# Filter for availability zones
if availability_zone_hints:
LOG.debug('Filtering Chassis candidates by availability zone '
'hints: %s', ', '.join(availability_zone_hints))
candidates = [ch for ch in candidates
for az in availability_zone_hints
if az in utils.get_chassis_availability_zones(
self._sb_idl.lookup('Chassis', ch, None))]

if not cms_bmaps:
LOG.debug("No eligible chassis with external connectivity"
" through ovn-cms-options for %s", physnet)
@@ -1331,9 +1345,9 @@ class OVNClient(object):

return options

def _create_lrouter_port(self, context, router_id, port, txn=None):
def _create_lrouter_port(self, context, router, port, txn=None):
"""Create a logical router port."""
lrouter = utils.ovn_name(router_id)
lrouter = utils.ovn_name(router['id'])
networks, ipv6_ra_configs = (
self._get_nets_and_ipv6_ra_confs_for_router_port(
context, port['fixed_ips']))
@@ -1347,7 +1361,8 @@ class OVNClient(object):
port_net = self._plugin.get_network(n_context.get_admin_context(),
port['network_id'])
physnet = self._get_physnet(port_net)
candidates = self.get_candidates_for_scheduling(physnet)
candidates = self.get_candidates_for_scheduling(
physnet, availability_zone_hints=utils.get_az_hints(router))
selected_chassis = self._ovn_scheduler.select(
self._nb_idl, self._sb_idl, lrouter_port_name,
candidates=candidates)
@@ -1374,6 +1389,7 @@ class OVNClient(object):

def create_router_port(self, context, router_id, router_interface):
port = self._plugin.get_port(context, router_interface['port_id'])
router = self._l3_plugin.get_router(context, router_id)
with self._nb_idl.transaction(check_error=True) as txn:
multi_prefix = False
if (len(router_interface.get('subnet_ids', [])) == 1 and
@@ -1385,9 +1401,8 @@ class OVNClient(object):
self._update_lrouter_port(context, port, txn=txn)
multi_prefix = True
else:
self._create_lrouter_port(context, router_id, port, txn=txn)
self._create_lrouter_port(context, router, port, txn=txn)

router = self._l3_plugin.get_router(context, router_id)
if router.get(l3.EXTERNAL_GW_INFO):
cidr = None
for fixed_ip in port['fixed_ips']:


+ 5
- 2
neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_db_sync.py View File

@@ -439,11 +439,13 @@ class OvnNbSynchronizer(OvnDbSynchronizer):
update_snats_list.append({'id': lrouter['name'],
'add': add_snats,
'del': del_snats})
del db_routers[lrouter['name']]
else:
del_lrouters_list.append(lrouter)

lrouters_names = {lr['name'] for lr in lrouters}
for r_id, router in db_routers.items():
if r_id in lrouters_names:
continue
LOG.warning("Router found in Neutron but not in "
"OVN DB, router id=%s", router['id'])
if self.mode == SYNC_MODE_REPAIR:
@@ -482,8 +484,9 @@ class OvnNbSynchronizer(OvnDbSynchronizer):
try:
LOG.warning("Creating the router port %s in OVN NB DB",
rrport['id'])
router = db_routers[rrport['device_id']]
self._ovn_client._create_lrouter_port(
ctx, rrport['device_id'], rrport)
ctx, router, rrport)
except RuntimeError:
LOG.warning("Create router port in OVN "
"NB failed for router port %s", rrport['id'])


+ 45
- 2
neutron/services/ovn_l3/plugin.py View File

@@ -26,6 +26,7 @@ from neutron_lib.callbacks import resources
from neutron_lib import constants as n_const
from neutron_lib import context as n_context
from neutron_lib import exceptions as n_exc
from neutron_lib.exceptions import availability_zone as az_exc
from neutron_lib.plugins import constants as plugin_constants
from neutron_lib.plugins import directory
from neutron_lib.services import base as service_base
@@ -35,6 +36,7 @@ from oslo_utils import excutils
from neutron.common.ovn import constants as ovn_const
from neutron.common.ovn import extensions
from neutron.common.ovn import utils
from neutron.db.availability_zone import router as router_az_db
from neutron.db import l3_fip_port_details
from neutron.db import ovn_revision_numbers_db as db_rev
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_client
@@ -49,7 +51,8 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase,
extraroute_db.ExtraRoute_dbonly_mixin,
l3_gwmode_db.L3_NAT_db_mixin,
dns_db.DNSDbMixin,
l3_fip_port_details.Fip_port_details_db_mixin):
l3_fip_port_details.Fip_port_details_db_mixin,
router_az_db.RouterAvailabilityZoneMixin):
"""Implementation of the OVN L3 Router Service Plugin.

This class implements a L3 service plugin that provides
@@ -311,6 +314,22 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase,
if port['status'] != status:
self._plugin.update_port_status(context, port['id'], status)

def _get_availability_zones_from_router_port(self, lrp_name):
"""Return the availability zones hints for the router port.

Return a list of availability zones hints associated with the
router that the router port belongs to.
"""
context = n_context.get_admin_context()
if not self._plugin_driver.list_availability_zones(context):
return []

lrp = self._ovn.get_lrouter_port(lrp_name)
router = self.get_router(
context, lrp.external_ids[ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY])
az_hints = utils.get_az_hints(router)
return az_hints

def schedule_unhosted_gateways(self, event_from_chassis=None):
# GW ports and its physnets.
port_physnet_dict = self._get_gateway_port_physnet_mapping()
@@ -350,9 +369,11 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase,
nb_idl=self._ovn, gw_chassis=all_gw_chassis,
physnet=physnet, chassis_physnets=chassis_with_physnets,
existing_chassis=existing_chassis)
az_hints = self._get_availability_zones_from_router_port(g_name)
candidates = self._ovn_client.get_candidates_for_scheduling(
physnet, cms=all_gw_chassis,
chassis_physnets=chassis_with_physnets)
chassis_physnets=chassis_with_physnets,
availability_zone_hints=az_hints)
chassis = self.scheduler.select(
self._ovn, self._sb_ovn, g_name, candidates=candidates,
existing_chassis=existing_chassis)
@@ -422,3 +443,25 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase,
# the OVN NB DB side
l3plugin._ovn_client.update_router_port(kwargs['context'],
current, if_exists=True)

def get_router_availability_zones(self, router):
lr = self._ovn.get_lrouter(router['id'])
if not lr:
return []

return [az.strip() for az in lr.external_ids.get(
ovn_const.OVN_ROUTER_AZ_HINTS_EXT_ID_KEY, '').split(',')
if az.strip()]

def validate_availability_zones(self, context, resource_type,
availability_zones):
"""Verify that the availability zones exist."""
if not availability_zones or resource_type != 'router':
return

azs = {az['name'] for az in
self._plugin_driver.list_availability_zones(context).values()}
diff = set(availability_zones) - azs
if diff:
raise az_exc.AvailabilityZoneNotFound(
availability_zone=', '.join(diff))

+ 28
- 0
neutron/tests/unit/common/ovn/test_utils.py View File

@@ -61,6 +61,34 @@ class TestUtils(base.BaseTestCase):
self.assertFalse(utils.is_gateway_chassis(non_gw_chassis_1))
self.assertFalse(utils.is_gateway_chassis(non_gw_chassis_2))

def test_get_chassis_availability_zones_no_azs(self):
chassis = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'external_ids': {'ovn-cms-options': 'enable-chassis-as-gw'}})
self.assertEqual([], utils.get_chassis_availability_zones(chassis))

def test_get_chassis_availability_zones_one_az(self):
chassis = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'external_ids': {'ovn-cms-options':
'enable-chassis-as-gw,availability-zones=az0'}})
self.assertEqual(
['az0'], utils.get_chassis_availability_zones(chassis))

def test_get_chassis_availability_zones_multiple_az(self):
chassis = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'external_ids': {
'ovn-cms-options':
'enable-chassis-as-gw,availability-zones=az0:az1 :az2:: :'}})
self.assertEqual(
['az0', 'az1', 'az2'],
utils.get_chassis_availability_zones(chassis))

def test_get_chassis_availability_zones_malformed(self):
chassis = fakes.FakeOvsdbRow.create_one_ovsdb_row(attrs={
'external_ids': {'ovn-cms-options':
'enable-chassis-as-gw,availability-zones:az0'}})
self.assertEqual(
[], utils.get_chassis_availability_zones(chassis))


class TestGateWayChassisValidity(base.BaseTestCase):



+ 32
- 0
neutron/tests/unit/fake_resources.py View File

@@ -161,6 +161,8 @@ class FakeOvsdbSbOvnIdl(object):
self.is_col_present = mock.Mock()
self.is_col_present.return_value = False
self.db_set = mock.Mock()
self.lookup = mock.MagicMock()
self.chassis_list = mock.MagicMock()


class FakeOvsdbTransaction(object):
@@ -751,3 +753,33 @@ class FakeOVNRouter(object):
'enabled': router.get('admin_state_up') or False,
'name': ovn_utils.ovn_name(router['id']),
'static_routes': routes})


class FakeChassis(object):

@staticmethod
def create(attrs=None, az_list=None, chassis_as_gw=False):
cms_opts = []
if az_list:
cms_opts.append("%s=%s" % (ovn_const.CMS_OPT_AVAILABILITY_ZONES,
':'.join(az_list)))
if chassis_as_gw:
cms_opts.append(ovn_const.CMS_OPT_CHASSIS_AS_GW)

external_ids = {}
if cms_opts:
external_ids[ovn_const.OVN_CMS_OPTIONS] = ','.join(cms_opts)

attrs = {
'encaps': [],
'external_ids': external_ids,
'hostname': '',
'name': uuidutils.generate_uuid(),
'nb_cfg': 0,
'other_config': {},
'transport_zones': [],
'vtep_logical_switches': []}

# Overwrite default attributes.
attrs.update(attrs)
return type('Chassis', (object, ), attrs)

+ 3
- 3
neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_ovn_db_sync.py View File

@@ -664,9 +664,9 @@ class TestOvnNbSyncML2(test_mech_driver.OVNMechanismDriverTestCase):
ovn_nb_synchronizer._ovn_client.create_router.assert_has_calls(
create_router_calls, any_order=True)

create_router_port_calls = [mock.call(mock.ANY, p['device_id'],
mock.ANY)
for p in create_router_port_list]
create_router_port_calls = [
mock.call(mock.ANY, self.routers[i], mock.ANY)
for i, p in enumerate(create_router_port_list)]
self.assertEqual(
len(create_router_port_list),
ovn_nb_synchronizer._ovn_client._create_lrouter_port.call_count)


+ 72
- 0
neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py View File

@@ -1692,6 +1692,43 @@ class TestOVNMechanismDriver(test_plugin.Ml2PluginV2TestCase):
# Assert that ping_chassis returned False as it didn't update the db
self.assertFalse(update_db)

def test_get_candidates_for_scheduling_availability_zones(self):
ovn_client = self.mech_driver._ovn_client
ch0 = fakes.FakeChassis.create(az_list=['az0', 'az1'],
chassis_as_gw=True)
ch1 = fakes.FakeChassis.create(az_list=['az3', 'az4'],
chassis_as_gw=True)
ch2 = fakes.FakeChassis.create(az_list=['az2'], chassis_as_gw=True)
ch3 = fakes.FakeChassis.create(az_list=[], chassis_as_gw=True)
ch4 = fakes.FakeChassis.create(az_list=['az0'], chassis_as_gw=True)
ch5 = fakes.FakeChassis.create(az_list=['az2'], chassis_as_gw=False)

# Fake ovsdbapp lookup
def fake_lookup(table, chassis_name, default):
for ch in [ch0, ch1, ch2, ch3, ch4, ch5]:
if ch.name == chassis_name:
return ch
ovn_client._sb_idl.lookup = fake_lookup

# The target physnet and availability zones
physnet = 'public'
az_hints = ['az0', 'az2']

cms = [ch0.name, ch1.name, ch2.name, ch3.name, ch4.name, ch5.name]
ch_physnet = {ch0.name: [physnet], ch1.name: [physnet],
ch2.name: [physnet], ch3.name: [physnet],
ch4.name: ['another-physnet'],
ch5.name: ['yet-another-physnet']}

candidates = ovn_client.get_candidates_for_scheduling(
physnet, cms=cms, chassis_physnets=ch_physnet,
availability_zone_hints=az_hints)

# Only chassis ch0 and ch2 should match the availability zones
# hints and physnet we passed to get_candidates_for_scheduling()
expected_candidates = [ch0.name, ch2.name]
self.assertEqual(sorted(expected_candidates), sorted(candidates))


class OVNMechanismDriverTestCase(test_plugin.Ml2PluginV2TestCase):
_mechanism_drivers = ['logger', 'ovn']
@@ -3140,3 +3177,38 @@ class TestOVNVVirtualPort(OVNMechanismDriverTestCase):
self.mech_driver._ovn_client.delete_port(self.context, parent['id'])
self.nb_idl.unset_lswitch_port_to_virtual_type.assert_called_once_with(
virt_port['id'], parent['id'], if_exists=True)


class TestOVNAvailabilityZone(OVNMechanismDriverTestCase):

def setUp(self):
super(TestOVNAvailabilityZone, self).setUp()
self.context = context.get_admin_context()
self.sb_idl = self.mech_driver._ovn_client._sb_idl

def test_list_availability_zones(self):
ch0 = fakes.FakeChassis.create(az_list=['az0', 'az1'],
chassis_as_gw=True)
ch1 = fakes.FakeChassis.create(az_list=[], chassis_as_gw=False)
ch2 = fakes.FakeChassis.create(az_list=['az2'], chassis_as_gw=True)
ch3 = fakes.FakeChassis.create(az_list=[], chassis_as_gw=True)
self.sb_idl.chassis_list.return_value.execute.return_value = [
ch0, ch1, ch2, ch3]

azs = self.mech_driver.list_availability_zones(self.context)
expected_azs = {'az0': {'name': 'az0', 'resource': 'router',
'state': 'available', 'tenant_id': mock.ANY},
'az1': {'name': 'az1', 'resource': 'router',
'state': 'available', 'tenant_id': mock.ANY},
'az2': {'name': 'az2', 'resource': 'router',
'state': 'available', 'tenant_id': mock.ANY}}
self.assertEqual(expected_azs, azs)

def test_list_availability_zones_no_azs(self):
ch0 = fakes.FakeChassis.create(az_list=[], chassis_as_gw=True)
ch1 = fakes.FakeChassis.create(az_list=[], chassis_as_gw=True)
self.sb_idl.chassis_list.return_value.execute.return_value = [
ch0, ch1]

azs = self.mech_driver.list_availability_zones(mock.Mock())
self.assertEqual({}, azs)

+ 60
- 4
neutron/tests/unit/services/ovn_l3/test_plugin.py View File

@@ -22,6 +22,7 @@ from neutron_lib.callbacks import events
from neutron_lib.callbacks import resources
from neutron_lib import constants
from neutron_lib import exceptions as n_exc
from neutron_lib.exceptions import availability_zone as az_exc
from neutron_lib.plugins import constants as plugin_constants
from neutron_lib.plugins import directory
from oslo_config import cfg
@@ -43,6 +44,7 @@ from neutron.tests.unit.plugins.ml2 import test_plugin as test_mech_driver
# Ml2PluginV2TestCase.
class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):

_mechanism_drivers = ['ovn']
l3_plugin = 'neutron.services.ovn_l3.plugin.OVNL3RouterPlugin'

def _start_mock(self, path, return_value, new_callable=None):
@@ -384,7 +386,8 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
'neutron-router-id', enabled=True, external_ids={
ovn_const.OVN_GW_PORT_EXT_ID_KEY: '',
ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1',
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'router'})
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'router',
ovn_const.OVN_ROUTER_AZ_HINTS_EXT_ID_KEY: ''})

@mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.'
'update_router')
@@ -402,7 +405,8 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
'neutron-router-id', enabled=False,
external_ids={ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'test',
ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1',
ovn_const.OVN_GW_PORT_EXT_ID_KEY: ''})
ovn_const.OVN_GW_PORT_EXT_ID_KEY: '',
ovn_const.OVN_ROUTER_AZ_HINTS_EXT_ID_KEY: ''})

@mock.patch.object(utils, 'get_lrouter_non_gw_routes')
@mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.update_router')
@@ -495,7 +499,8 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):

external_ids = {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'router',
ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1',
ovn_const.OVN_GW_PORT_EXT_ID_KEY: 'gw-port-id'}
ovn_const.OVN_GW_PORT_EXT_ID_KEY: 'gw-port-id',
ovn_const.OVN_ROUTER_AZ_HINTS_EXT_ID_KEY: ''}
self.l3_inst._ovn.create_lrouter.assert_called_once_with(
'neutron-router-id', external_ids=external_ids,
enabled=True, options={})
@@ -1351,6 +1356,8 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
self.l3_inst.schedule_unhosted_gateways()
self.nb_idl().update_lrouter_port.assert_not_called()

@mock.patch('neutron.plugins.ml2.drivers.ovn.mech_driver.mech_driver.'
'OVNMechanismDriver.list_availability_zones', lambda *_: [])
@mock.patch('neutron.services.ovn_l3.plugin.OVNL3RouterPlugin.'
'_get_gateway_port_physnet_mapping')
def test_schedule_unhosted_gateways(self, get_gppm):
@@ -1389,7 +1396,7 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
self.mock_candidates.assert_has_calls([
mock.call(mock.ANY,
chassis_physnets=chassis_mappings,
cms=chassis)] * 3)
cms=chassis, availability_zone_hints=[])] * 3)
self.mock_schedule.assert_has_calls([
mock.call(self.nb_idl(), self.sb_idl(),
'lrp-foo-1', [], ['chassis1', 'chassis2']),
@@ -1473,6 +1480,55 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
mock.ANY, self.fake_router_port,
ovn_const.TYPE_ROUTER_PORTS)

def _test_get_router_availability_zones(self, azs, expected):
lr = fake_resources.FakeOvsdbRow.create_one_ovsdb_row(
attrs={'id': 'fake-router', 'external_ids': {
ovn_const.OVN_ROUTER_AZ_HINTS_EXT_ID_KEY: azs}})
self.l3_inst._ovn.get_lrouter.return_value = lr
azs_list = self.l3_inst.get_router_availability_zones(lr)
self.assertEqual(sorted(expected), sorted(azs_list))

def test_get_router_availability_zones_one(self):
self._test_get_router_availability_zones('az0', ['az0'])

def test_get_router_availability_zones_multiple(self):
self._test_get_router_availability_zones(
'az0,az1,az2', ['az0', 'az1', 'az2'])

def test_get_router_availability_zones_none(self):
self._test_get_router_availability_zones('', [])

@mock.patch('neutron.plugins.ml2.drivers.ovn.mech_driver.mech_driver.'
'OVNMechanismDriver.list_availability_zones')
def test_validate_availability_zones(self, mock_list_azs):
mock_list_azs.return_value = {'az0': {'name': 'az0'},
'az1': {'name': 'az1'},
'az2': {'name': 'az2'}}
self.assertIsNone(
self.l3_inst.validate_availability_zones(
self.context, 'router', ['az0', 'az2']))

@mock.patch('neutron.plugins.ml2.drivers.ovn.mech_driver.mech_driver.'
'OVNMechanismDriver.list_availability_zones')
def test_validate_availability_zones_fail_non_exist(self, mock_list_azs):
mock_list_azs.return_value = {'az0': {'name': 'az0'},
'az1': {'name': 'az1'},
'az2': {'name': 'az2'}}
# Fails validation if the az does not exist
self.assertRaises(
az_exc.AvailabilityZoneNotFound,
self.l3_inst.validate_availability_zones, self.context, 'router',
['az0', 'non-existent'])

@mock.patch('neutron.plugins.ml2.drivers.ovn.mech_driver.mech_driver.'
'OVNMechanismDriver.list_availability_zones')
def test_validate_availability_zones_no_azs(self, mock_list_azs):
# When no AZs are requested validation should just succeed
self.assertIsNone(
self.l3_inst.validate_availability_zones(
self.context, 'router', []))
mock_list_azs.assert_not_called()


class OVNL3ExtrarouteTests(test_l3_gw.ExtGwModeIntTestCase,
test_l3.L3NatDBIntTestCase,


+ 6
- 0
releasenotes/notes/ovn-router-availability-zones-03a802ee19689474.yaml View File

@@ -0,0 +1,6 @@
---
features:
- |
Added support for router availability zones in OVN. The OVN driver
can now read from the router's availability_zone_hints field and
schedule router ports accordingly with the given availability zones.

Loading…
Cancel
Save