Browse Source

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.

(cherry picked from Neutron d669dff1dc)

Change-Id: I04858cb7a38da083962449779b6063f0c48f3ae7
Partial-Bug: #1881095
Signed-off-by: Lucas Alvares Gomes <lucasagomes@gmail.com>
changes/05/738405/2
Lucas Alvares Gomes 1 month ago
committed by Bernard Cafarelli
parent
commit
44e5a6b9e3
13 changed files with 354 additions and 26 deletions
  1. +6
    -0
      networking_ovn/common/constants.py
  2. +7
    -1
      networking_ovn/common/extensions.py
  3. +25
    -10
      networking_ovn/common/ovn_client.py
  4. +32
    -3
      networking_ovn/common/utils.py
  5. +45
    -2
      networking_ovn/l3/l3_ovn.py
  6. +33
    -1
      networking_ovn/ml2/mech_driver.py
  7. +5
    -2
      networking_ovn/ovn_db_sync.py
  8. +28
    -0
      networking_ovn/tests/unit/common/test_utils.py
  9. +32
    -0
      networking_ovn/tests/unit/fakes.py
  10. +60
    -4
      networking_ovn/tests/unit/l3/test_l3_ovn.py
  11. +72
    -0
      networking_ovn/tests/unit/ml2/test_mech_driver.py
  12. +3
    -3
      networking_ovn/tests/unit/test_ovn_db_sync.py
  13. +6
    -0
      releasenotes/notes/ovn-router-availability-zones-03a802ee19689474.yaml

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

@@ -27,6 +27,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'
@@ -289,3 +290,8 @@ LB_SELECTION_FIELDS_MAP = {
constants.LB_ALGORITHM_SOURCE_IP: ["ip_src", "ip_dst"],
None: ["ip_src", "ip_dst", "tp_src", "tp_dst"],
}

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'

+ 7
- 1
networking_ovn/common/extensions.py View File

@@ -15,6 +15,10 @@
# to be able to easily import it and export the info without any dependencies
# on external imports.

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) If you update these lists, please also update
# doc/source/features.rst and the current release note.
ML2_SUPPORTED_API_EXTENSIONS_NEUTRON_L3 = [
@@ -35,13 +39,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',


+ 25
- 10
networking_ovn/common/ovn_client.py View File

@@ -1160,12 +1160,11 @@ class OVNClient(object):
return list(networks), ipv6_ra_configs

def _add_router_ext_gw(self, context, router, networks, txn):
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(router_id, port, txn=txn)
self._create_lrouter_port(router, port, txn=txn)

def _build_extids(gw_info):
# TODO(lucasagomes): Remove this check after OVS 2.8.2 is tagged
@@ -1180,7 +1179,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(
@@ -1273,7 +1272,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, router, add_external_gateway=True):
"""Create a logical router."""
@@ -1396,13 +1397,16 @@ class OVNClient(object):
db_rev.delete_revision(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
@@ -1420,6 +1424,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)
@@ -1468,9 +1482,9 @@ class OVNClient(object):

return options

def _create_lrouter_port(self, router_id, port, txn=None):
def _create_lrouter_port(self, 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(
port['fixed_ips']))
@@ -1484,7 +1498,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)
@@ -1512,6 +1527,7 @@ class OVNClient(object):
def create_router_port(self, router_id, router_interface):
context = n_context.get_admin_context()
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
@@ -1523,9 +1539,8 @@ class OVNClient(object):
self._update_lrouter_port(port, txn=txn)
multi_prefix = True
else:
self._create_lrouter_port(router_id, port, txn=txn)
self._create_lrouter_port(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']:


+ 32
- 3
networking_ovn/common/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,8 @@ from networking_ovn.common import exceptions as ovn_exc

LOG = log.getLogger(__name__)

CONF = cfg.CONF

DNS_RESOLVER_FILE = "/etc/resolv.conf"

AddrPairsDiff = collections.namedtuple(
@@ -488,11 +492,15 @@ def is_neutron_dhcp_agent_port(port):
port['device_id'].startswith('dhcp')))


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):
@@ -512,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

+ 45
- 2
networking_ovn/l3/l3_ovn.py View File

@@ -12,6 +12,7 @@
# under the License.
#

from neutron.db.availability_zone import router as router_az_db
from neutron.db import dns_db
from neutron.db import extraroute_db
from neutron.db import l3_gwmode_db
@@ -26,6 +27,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
@@ -47,7 +49,8 @@ LOG = log.getLogger(__name__)
class OVNL3RouterPlugin(service_base.ServicePluginBase,
extraroute_db.ExtraRoute_dbonly_mixin,
l3_gwmode_db.L3_NAT_db_mixin,
dns_db.DNSDbMixin):
dns_db.DNSDbMixin,
router_az_db.RouterAvailabilityZoneMixin):
"""Implementation of the OVN L3 Router Service Plugin.

This class implements a L3 service plugin that provides
@@ -306,6 +309,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()
@@ -345,9 +364,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)
@@ -416,3 +437,25 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase,
# trigger this callback even before we had the chance to create
# the OVN NB DB side
l3plugin._ovn_client.update_router_port(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))

+ 33
- 1
networking_ovn/ml2/mech_driver.py View File

@@ -238,6 +238,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()

@@ -1032,7 +1036,8 @@ class OVNMechanismDriver(api.MechanismDriver):
'binary': binary,
'host': chassis.hostname,
'heartbeat_timestamp': timeutils.utcnow(),
'availability_zone': 'n/a',
'availability_zone': ', '.join(
utils.get_chassis_availability_zones(chassis)),
'topic': 'n/a',
'description': description,
'configurations': {
@@ -1121,6 +1126,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 utils.is_gateway_chassis(ch):
continue

azones = 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():
@@ -1185,3 +1211,9 @@ def delete_agent(self, context, id, _driver=None):
get_agent(self, None, id, _driver=_driver)
raise n_exc.BadRequest(resource='agent',
msg='OVN agents cannot be deleted')


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

+ 5
- 2
networking_ovn/ovn_db_sync.py View File

@@ -602,11 +602,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:
@@ -645,8 +647,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(
rrport['device_id'], rrport)
router, rrport)
except RuntimeError:
LOG.warning("Create router port in OVN "
"NB failed for router port %s", rrport['id'])


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

@@ -60,6 +60,34 @@ class TestUtils(base.TestCase):
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.TestCase):



+ 32
- 0
networking_ovn/tests/unit/fakes.py View File

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


class FakeOvsdbTransaction(object):
@@ -759,3 +761,33 @@ class FakeOVNRouter(object):
'enabled': router.get('admin_state_up') or False,
'name': 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)

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

@@ -27,6 +27,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
@@ -41,6 +42,7 @@ from networking_ovn.tests.unit.ml2 import test_mech_driver

class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):

_mechanism_drivers = ['ovn']
l3_plugin = 'networking_ovn.l3.l3_ovn.OVNL3RouterPlugin'

def _start_mock(self, path, return_value, new_callable=None):
@@ -372,7 +374,8 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
'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')
@@ -392,7 +395,8 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
'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')
@@ -462,7 +466,8 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):

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={})
@@ -1371,6 +1376,8 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
self.l3_inst.schedule_unhosted_gateways()
self.nb_idl().update_lrouter_port.assert_not_called()

@mock.patch('networking_ovn.ml2.mech_driver.'
'OVNMechanismDriver.list_availability_zones', lambda *_: [])
@mock.patch('networking_ovn.l3.l3_ovn.OVNL3RouterPlugin.'
'_get_gateway_port_physnet_mapping')
def test_schedule_unhosted_gateways(self, get_gppm):
@@ -1409,7 +1416,7 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
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']),
@@ -1498,6 +1505,55 @@ class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase):
self.bump_rev_p.assert_called_with(self.fake_router_port,
ovn_const.TYPE_ROUTER_PORTS)

def _test_get_router_availability_zones(self, azs, expected):
lr = fakes.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('networking_ovn.ml2.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('networking_ovn.ml2.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('networking_ovn.ml2.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,


+ 72
- 0
networking_ovn/tests/unit/ml2/test_mech_driver.py View File

@@ -1794,6 +1794,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']
@@ -3243,3 +3280,38 @@ class TestOVNVVirtualPort(OVNMechanismDriverTestCase):
self.mech_driver._ovn_client.delete_port(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)

+ 3
- 3
networking_ovn/tests/unit/test_ovn_db_sync.py View File

@@ -724,9 +724,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(p['device_id'],
mock.ANY)
for p in create_router_port_list]
create_router_port_calls = [
mock.call(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)


+ 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