[OVN] Add support for router availability zones

This patch is adding support for the router_availability_zone extension
for Neutron.

The OVN driver will now read from the router's availability_zone_hints
field and schedule the router ports onto OVN chassis belonging to those
AZs.

Since the OVN driver does not rely on the L3 agent, this patch does not
re-use the configuration option for the agent to configure the
availability zone that a Chassis belongs to (even because there's no
configuration file in nodes such as networker nodes). Instead, this
patch reuses the "ovn-cms-options" field from the local OVSDB to
configure the Chassis. The follow syntax has been used:

$ ovs-vsctl set Open_VSwitch .
external-ids:ovn-cms-options="enable-chassis-as-gw,availability-zones=az0:az1"

In the example above, the Chassis has been configured to belong to two
AZs: "az0" and "az1".

This patch also implements listing the availability zones:
$ openstack availability zone list

As well as validating the router's availability zone hints:
$ openstack router create --availability-zone-hint az0
--availability-zone-hint az1 test_router

The above command would fail if there's no "az0" and "az1" configured in
any OVN chassis.

Documentation for this feature is being written and will be submitted
in a separated patch.

Partial-Bug: #1881095
Change-Id: I4567f3d541d382b6432c1ab3d35276d81ce71d82
Signed-off-by: Lucas Alvares Gomes <lucasagomes@gmail.com>
This commit is contained in:
Lucas Alvares Gomes 2020-05-12 12:02:15 +01:00
parent b1dba996b5
commit d669dff1dc
13 changed files with 354 additions and 26 deletions

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'

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',

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

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())

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']:

View File

@ -434,11 +434,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:
@ -477,8 +479,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'])

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))

View File

@ -60,6 +60,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):

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)

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)

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)

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,

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.