[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:
parent
b1dba996b5
commit
d669dff1dc
@ -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'
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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']:
|
||||
|
@ -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'])
|
||||
|
@ -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))
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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…
Reference in New Issue
Block a user