[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_NAME_EXT_ID_KEY = 'neutron:port_name'
|
||||||
OVN_PORT_FIP_EXT_ID_KEY = 'neutron:port_fip'
|
OVN_PORT_FIP_EXT_ID_KEY = 'neutron:port_fip'
|
||||||
OVN_ROUTER_NAME_EXT_ID_KEY = 'neutron:router_name'
|
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_ROUTER_IS_EXT_GW = 'neutron:is_ext_gw'
|
||||||
OVN_GW_PORT_EXT_ID_KEY = 'neutron:gw_port_id'
|
OVN_GW_PORT_EXT_ID_KEY = 'neutron:gw_port_id'
|
||||||
OVN_SUBNET_EXT_ID_KEY = 'neutron:subnet_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,
|
EXTERNAL_PORT_TYPES = (portbindings.VNIC_DIRECT,
|
||||||
portbindings.VNIC_DIRECT_PHYSICAL,
|
portbindings.VNIC_DIRECT_PHYSICAL,
|
||||||
portbindings.VNIC_MACVTAP)
|
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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# 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
|
# 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
|
# to be able to easily import it and export the info without any dependencies
|
||||||
# on external imports.
|
# on external imports.
|
||||||
@ -26,13 +31,15 @@ ML2_SUPPORTED_API_EXTENSIONS_OVN_L3 = [
|
|||||||
'sorting',
|
'sorting',
|
||||||
'project-id',
|
'project-id',
|
||||||
'dns-integration',
|
'dns-integration',
|
||||||
|
agent_def.ALIAS,
|
||||||
|
az_def.ALIAS,
|
||||||
|
raz_def.ALIAS,
|
||||||
]
|
]
|
||||||
ML2_SUPPORTED_API_EXTENSIONS = [
|
ML2_SUPPORTED_API_EXTENSIONS = [
|
||||||
'address-scope',
|
'address-scope',
|
||||||
'agent',
|
'agent',
|
||||||
'allowed-address-pairs',
|
'allowed-address-pairs',
|
||||||
'auto-allocated-topology',
|
'auto-allocated-topology',
|
||||||
'availability_zone',
|
|
||||||
'binding',
|
'binding',
|
||||||
'default-subnetpools',
|
'default-subnetpools',
|
||||||
'external-net',
|
'external-net',
|
||||||
|
@ -16,6 +16,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
import netaddr
|
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 external_net
|
||||||
from neutron_lib.api.definitions import extra_dhcp_opt as edo_ext
|
from neutron_lib.api.definitions import extra_dhcp_opt as edo_ext
|
||||||
from neutron_lib.api.definitions import l3
|
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 import exceptions as n_exc
|
||||||
from neutron_lib.plugins import directory
|
from neutron_lib.plugins import directory
|
||||||
from neutron_lib.utils import net as n_utils
|
from neutron_lib.utils import net as n_utils
|
||||||
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from oslo_utils import netutils
|
from oslo_utils import netutils
|
||||||
from oslo_utils import strutils
|
from oslo_utils import strutils
|
||||||
@ -40,6 +42,7 @@ from neutron.common.ovn import exceptions as ovn_exc
|
|||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
DNS_RESOLVER_FILE = "/etc/resolv.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))
|
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):
|
def is_gateway_chassis(chassis):
|
||||||
"""Check if the given chassis is a gateway chassis"""
|
"""Check if the given chassis is a gateway chassis"""
|
||||||
external_ids = getattr(chassis, 'external_ids', {})
|
return constants.CMS_OPT_CHASSIS_AS_GW in get_ovn_cms_options(chassis)
|
||||||
return ('enable-chassis-as-gw' in external_ids.get(
|
|
||||||
'ovn-cms-options', '').split(','))
|
|
||||||
|
|
||||||
|
|
||||||
def get_port_capabilities(port):
|
def get_port_capabilities(port):
|
||||||
@ -513,3 +520,24 @@ def get_port_id_from_gwc_row(row):
|
|||||||
:returns: String containing router port_id.
|
:returns: String containing router port_id.
|
||||||
"""
|
"""
|
||||||
return constants.RE_PORT_FROM_GWC.search(row.name).group(2)
|
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("update_agent", update_agent)
|
||||||
self.patch_plugin_choose("delete_agent", delete_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.
|
# Now IDL connections can be safely used.
|
||||||
self._post_fork_event.set()
|
self._post_fork_event.set()
|
||||||
|
|
||||||
@ -1091,7 +1095,8 @@ class OVNMechanismDriver(api.MechanismDriver):
|
|||||||
'binary': binary,
|
'binary': binary,
|
||||||
'host': chassis.hostname,
|
'host': chassis.hostname,
|
||||||
'heartbeat_timestamp': timeutils.utcnow(),
|
'heartbeat_timestamp': timeutils.utcnow(),
|
||||||
'availability_zone': 'n/a',
|
'availability_zone': ', '.join(
|
||||||
|
ovn_utils.get_chassis_availability_zones(chassis)),
|
||||||
'topic': 'n/a',
|
'topic': 'n/a',
|
||||||
'description': description,
|
'description': description,
|
||||||
'configurations': {
|
'configurations': {
|
||||||
@ -1180,6 +1185,27 @@ class OVNMechanismDriver(api.MechanismDriver):
|
|||||||
txn.add(self._nb_ovn.check_liveness())
|
txn.add(self._nb_ovn.check_liveness())
|
||||||
return True
|
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):
|
def populate_agents(driver):
|
||||||
for ch in driver._sb_ovn.tables['Chassis'].rows.values():
|
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:
|
if ports_with_pg:
|
||||||
# Add the ports to the default Port Group
|
# Add the ports to the default Port Group
|
||||||
txn.add(nb_idl.pg_add_ports(pg_name, list(ports_with_pg)))
|
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):
|
def _add_router_ext_gw(self, router, networks, txn):
|
||||||
context = n_context.get_admin_context()
|
context = n_context.get_admin_context()
|
||||||
router_id = router['id']
|
|
||||||
# 1. Add the external gateway router port.
|
# 1. Add the external gateway router port.
|
||||||
gateways = self._get_gw_info(context, router)
|
gateways = self._get_gw_info(context, router)
|
||||||
gw_port_id = router['gw_port_id']
|
gw_port_id = router['gw_port_id']
|
||||||
port = self._plugin.get_port(context, 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):
|
def _build_extids(gw_info):
|
||||||
# TODO(lucasagomes): Remove this check after OVS 2.8.2 is tagged
|
# TODO(lucasagomes): Remove this check after OVS 2.8.2 is tagged
|
||||||
@ -1044,7 +1043,7 @@ class OVNClient(object):
|
|||||||
return columns
|
return columns
|
||||||
|
|
||||||
# 2. Add default route with nexthop as gateway ip
|
# 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:
|
for gw_info in gateways:
|
||||||
columns = _build_extids(gw_info)
|
columns = _build_extids(gw_info)
|
||||||
txn.add(self._nb_idl.add_static_route(
|
txn.add(self._nb_idl.add_static_route(
|
||||||
@ -1138,7 +1137,9 @@ class OVNClient(object):
|
|||||||
ovn_const.OVN_GW_PORT_EXT_ID_KEY:
|
ovn_const.OVN_GW_PORT_EXT_ID_KEY:
|
||||||
router.get('gw_port_id') or '',
|
router.get('gw_port_id') or '',
|
||||||
ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(utils.get_revision_number(
|
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):
|
def create_router(self, context, router, add_external_gateway=True):
|
||||||
"""Create a logical router."""
|
"""Create a logical router."""
|
||||||
@ -1259,13 +1260,16 @@ class OVNClient(object):
|
|||||||
db_rev.delete_revision(context, router_id, ovn_const.TYPE_ROUTERS)
|
db_rev.delete_revision(context, router_id, ovn_const.TYPE_ROUTERS)
|
||||||
|
|
||||||
def get_candidates_for_scheduling(self, physnet, cms=None,
|
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.
|
"""Return chassis for scheduling gateway router.
|
||||||
|
|
||||||
Criteria for selecting chassis as candidates
|
Criteria for selecting chassis as candidates
|
||||||
1) chassis from cms with proper bridge mappings
|
1) chassis from cms with proper bridge mappings
|
||||||
2) if no chassis is available from 1) then,
|
2) if no chassis is available from 1) then,
|
||||||
select chassis with proper bridge mappings
|
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
|
# TODO(lucasagomes): Simplify the logic here, the CMS option has
|
||||||
# been introduced long ago and by now all gateway chassis should
|
# been introduced long ago and by now all gateway chassis should
|
||||||
@ -1283,6 +1287,16 @@ class OVNClient(object):
|
|||||||
else:
|
else:
|
||||||
bmaps.append(chassis)
|
bmaps.append(chassis)
|
||||||
candidates = cms_bmaps or bmaps
|
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:
|
if not cms_bmaps:
|
||||||
LOG.debug("No eligible chassis with external connectivity"
|
LOG.debug("No eligible chassis with external connectivity"
|
||||||
" through ovn-cms-options for %s", physnet)
|
" through ovn-cms-options for %s", physnet)
|
||||||
@ -1331,9 +1345,9 @@ class OVNClient(object):
|
|||||||
|
|
||||||
return options
|
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."""
|
"""Create a logical router port."""
|
||||||
lrouter = utils.ovn_name(router_id)
|
lrouter = utils.ovn_name(router['id'])
|
||||||
networks, ipv6_ra_configs = (
|
networks, ipv6_ra_configs = (
|
||||||
self._get_nets_and_ipv6_ra_confs_for_router_port(
|
self._get_nets_and_ipv6_ra_confs_for_router_port(
|
||||||
context, port['fixed_ips']))
|
context, port['fixed_ips']))
|
||||||
@ -1347,7 +1361,8 @@ class OVNClient(object):
|
|||||||
port_net = self._plugin.get_network(n_context.get_admin_context(),
|
port_net = self._plugin.get_network(n_context.get_admin_context(),
|
||||||
port['network_id'])
|
port['network_id'])
|
||||||
physnet = self._get_physnet(port_net)
|
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(
|
selected_chassis = self._ovn_scheduler.select(
|
||||||
self._nb_idl, self._sb_idl, lrouter_port_name,
|
self._nb_idl, self._sb_idl, lrouter_port_name,
|
||||||
candidates=candidates)
|
candidates=candidates)
|
||||||
@ -1374,6 +1389,7 @@ class OVNClient(object):
|
|||||||
|
|
||||||
def create_router_port(self, context, router_id, router_interface):
|
def create_router_port(self, context, router_id, router_interface):
|
||||||
port = self._plugin.get_port(context, router_interface['port_id'])
|
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:
|
with self._nb_idl.transaction(check_error=True) as txn:
|
||||||
multi_prefix = False
|
multi_prefix = False
|
||||||
if (len(router_interface.get('subnet_ids', [])) == 1 and
|
if (len(router_interface.get('subnet_ids', [])) == 1 and
|
||||||
@ -1385,9 +1401,8 @@ class OVNClient(object):
|
|||||||
self._update_lrouter_port(context, port, txn=txn)
|
self._update_lrouter_port(context, port, txn=txn)
|
||||||
multi_prefix = True
|
multi_prefix = True
|
||||||
else:
|
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):
|
if router.get(l3.EXTERNAL_GW_INFO):
|
||||||
cidr = None
|
cidr = None
|
||||||
for fixed_ip in port['fixed_ips']:
|
for fixed_ip in port['fixed_ips']:
|
||||||
|
@ -434,11 +434,13 @@ class OvnNbSynchronizer(OvnDbSynchronizer):
|
|||||||
update_snats_list.append({'id': lrouter['name'],
|
update_snats_list.append({'id': lrouter['name'],
|
||||||
'add': add_snats,
|
'add': add_snats,
|
||||||
'del': del_snats})
|
'del': del_snats})
|
||||||
del db_routers[lrouter['name']]
|
|
||||||
else:
|
else:
|
||||||
del_lrouters_list.append(lrouter)
|
del_lrouters_list.append(lrouter)
|
||||||
|
|
||||||
|
lrouters_names = {lr['name'] for lr in lrouters}
|
||||||
for r_id, router in db_routers.items():
|
for r_id, router in db_routers.items():
|
||||||
|
if r_id in lrouters_names:
|
||||||
|
continue
|
||||||
LOG.warning("Router found in Neutron but not in "
|
LOG.warning("Router found in Neutron but not in "
|
||||||
"OVN DB, router id=%s", router['id'])
|
"OVN DB, router id=%s", router['id'])
|
||||||
if self.mode == SYNC_MODE_REPAIR:
|
if self.mode == SYNC_MODE_REPAIR:
|
||||||
@ -477,8 +479,9 @@ class OvnNbSynchronizer(OvnDbSynchronizer):
|
|||||||
try:
|
try:
|
||||||
LOG.warning("Creating the router port %s in OVN NB DB",
|
LOG.warning("Creating the router port %s in OVN NB DB",
|
||||||
rrport['id'])
|
rrport['id'])
|
||||||
|
router = db_routers[rrport['device_id']]
|
||||||
self._ovn_client._create_lrouter_port(
|
self._ovn_client._create_lrouter_port(
|
||||||
ctx, rrport['device_id'], rrport)
|
ctx, router, rrport)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
LOG.warning("Create router port in OVN "
|
LOG.warning("Create router port in OVN "
|
||||||
"NB failed for router port %s", rrport['id'])
|
"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 constants as n_const
|
||||||
from neutron_lib import context as n_context
|
from neutron_lib import context as n_context
|
||||||
from neutron_lib import exceptions as n_exc
|
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 constants as plugin_constants
|
||||||
from neutron_lib.plugins import directory
|
from neutron_lib.plugins import directory
|
||||||
from neutron_lib.services import base as service_base
|
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 constants as ovn_const
|
||||||
from neutron.common.ovn import extensions
|
from neutron.common.ovn import extensions
|
||||||
from neutron.common.ovn import utils
|
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 l3_fip_port_details
|
||||||
from neutron.db import ovn_revision_numbers_db as db_rev
|
from neutron.db import ovn_revision_numbers_db as db_rev
|
||||||
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovn_client
|
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,
|
extraroute_db.ExtraRoute_dbonly_mixin,
|
||||||
l3_gwmode_db.L3_NAT_db_mixin,
|
l3_gwmode_db.L3_NAT_db_mixin,
|
||||||
dns_db.DNSDbMixin,
|
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.
|
"""Implementation of the OVN L3 Router Service Plugin.
|
||||||
|
|
||||||
This class implements a L3 service plugin that provides
|
This class implements a L3 service plugin that provides
|
||||||
@ -311,6 +314,22 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase,
|
|||||||
if port['status'] != status:
|
if port['status'] != status:
|
||||||
self._plugin.update_port_status(context, port['id'], 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):
|
def schedule_unhosted_gateways(self, event_from_chassis=None):
|
||||||
# GW ports and its physnets.
|
# GW ports and its physnets.
|
||||||
port_physnet_dict = self._get_gateway_port_physnet_mapping()
|
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,
|
nb_idl=self._ovn, gw_chassis=all_gw_chassis,
|
||||||
physnet=physnet, chassis_physnets=chassis_with_physnets,
|
physnet=physnet, chassis_physnets=chassis_with_physnets,
|
||||||
existing_chassis=existing_chassis)
|
existing_chassis=existing_chassis)
|
||||||
|
az_hints = self._get_availability_zones_from_router_port(g_name)
|
||||||
candidates = self._ovn_client.get_candidates_for_scheduling(
|
candidates = self._ovn_client.get_candidates_for_scheduling(
|
||||||
physnet, cms=all_gw_chassis,
|
physnet, cms=all_gw_chassis,
|
||||||
chassis_physnets=chassis_with_physnets)
|
chassis_physnets=chassis_with_physnets,
|
||||||
|
availability_zone_hints=az_hints)
|
||||||
chassis = self.scheduler.select(
|
chassis = self.scheduler.select(
|
||||||
self._ovn, self._sb_ovn, g_name, candidates=candidates,
|
self._ovn, self._sb_ovn, g_name, candidates=candidates,
|
||||||
existing_chassis=existing_chassis)
|
existing_chassis=existing_chassis)
|
||||||
@ -422,3 +443,25 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase,
|
|||||||
# the OVN NB DB side
|
# the OVN NB DB side
|
||||||
l3plugin._ovn_client.update_router_port(kwargs['context'],
|
l3plugin._ovn_client.update_router_port(kwargs['context'],
|
||||||
current, if_exists=True)
|
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_1))
|
||||||
self.assertFalse(utils.is_gateway_chassis(non_gw_chassis_2))
|
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):
|
class TestGateWayChassisValidity(base.BaseTestCase):
|
||||||
|
|
||||||
|
@ -161,6 +161,8 @@ class FakeOvsdbSbOvnIdl(object):
|
|||||||
self.is_col_present = mock.Mock()
|
self.is_col_present = mock.Mock()
|
||||||
self.is_col_present.return_value = False
|
self.is_col_present.return_value = False
|
||||||
self.db_set = mock.Mock()
|
self.db_set = mock.Mock()
|
||||||
|
self.lookup = mock.MagicMock()
|
||||||
|
self.chassis_list = mock.MagicMock()
|
||||||
|
|
||||||
|
|
||||||
class FakeOvsdbTransaction(object):
|
class FakeOvsdbTransaction(object):
|
||||||
@ -751,3 +753,33 @@ class FakeOVNRouter(object):
|
|||||||
'enabled': router.get('admin_state_up') or False,
|
'enabled': router.get('admin_state_up') or False,
|
||||||
'name': ovn_utils.ovn_name(router['id']),
|
'name': ovn_utils.ovn_name(router['id']),
|
||||||
'static_routes': routes})
|
'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(
|
ovn_nb_synchronizer._ovn_client.create_router.assert_has_calls(
|
||||||
create_router_calls, any_order=True)
|
create_router_calls, any_order=True)
|
||||||
|
|
||||||
create_router_port_calls = [mock.call(mock.ANY, p['device_id'],
|
create_router_port_calls = [
|
||||||
mock.ANY)
|
mock.call(mock.ANY, self.routers[i], mock.ANY)
|
||||||
for p in create_router_port_list]
|
for i, p in enumerate(create_router_port_list)]
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
len(create_router_port_list),
|
len(create_router_port_list),
|
||||||
ovn_nb_synchronizer._ovn_client._create_lrouter_port.call_count)
|
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
|
# Assert that ping_chassis returned False as it didn't update the db
|
||||||
self.assertFalse(update_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):
|
class OVNMechanismDriverTestCase(test_plugin.Ml2PluginV2TestCase):
|
||||||
_mechanism_drivers = ['logger', 'ovn']
|
_mechanism_drivers = ['logger', 'ovn']
|
||||||
@ -3140,3 +3177,38 @@ class TestOVNVVirtualPort(OVNMechanismDriverTestCase):
|
|||||||
self.mech_driver._ovn_client.delete_port(self.context, parent['id'])
|
self.mech_driver._ovn_client.delete_port(self.context, parent['id'])
|
||||||
self.nb_idl.unset_lswitch_port_to_virtual_type.assert_called_once_with(
|
self.nb_idl.unset_lswitch_port_to_virtual_type.assert_called_once_with(
|
||||||
virt_port['id'], parent['id'], if_exists=True)
|
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.callbacks import resources
|
||||||
from neutron_lib import constants
|
from neutron_lib import constants
|
||||||
from neutron_lib import exceptions as n_exc
|
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 constants as plugin_constants
|
||||||
from neutron_lib.plugins import directory
|
from neutron_lib.plugins import directory
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
@ -43,6 +44,7 @@ from neutron.tests.unit.plugins.ml2 import test_plugin as test_mech_driver
|
|||||||
# Ml2PluginV2TestCase.
|
# Ml2PluginV2TestCase.
|
||||||
class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
|
class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
|
||||||
|
|
||||||
|
_mechanism_drivers = ['ovn']
|
||||||
l3_plugin = 'neutron.services.ovn_l3.plugin.OVNL3RouterPlugin'
|
l3_plugin = 'neutron.services.ovn_l3.plugin.OVNL3RouterPlugin'
|
||||||
|
|
||||||
def _start_mock(self, path, return_value, new_callable=None):
|
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={
|
'neutron-router-id', enabled=True, external_ids={
|
||||||
ovn_const.OVN_GW_PORT_EXT_ID_KEY: '',
|
ovn_const.OVN_GW_PORT_EXT_ID_KEY: '',
|
||||||
ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1',
|
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.'
|
@mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.'
|
||||||
'update_router')
|
'update_router')
|
||||||
@ -402,7 +405,8 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
|
|||||||
'neutron-router-id', enabled=False,
|
'neutron-router-id', enabled=False,
|
||||||
external_ids={ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'test',
|
external_ids={ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'test',
|
||||||
ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1',
|
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.object(utils, 'get_lrouter_non_gw_routes')
|
||||||
@mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.update_router')
|
@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',
|
external_ids = {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'router',
|
||||||
ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1',
|
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(
|
self.l3_inst._ovn.create_lrouter.assert_called_once_with(
|
||||||
'neutron-router-id', external_ids=external_ids,
|
'neutron-router-id', external_ids=external_ids,
|
||||||
enabled=True, options={})
|
enabled=True, options={})
|
||||||
@ -1351,6 +1356,8 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
|
|||||||
self.l3_inst.schedule_unhosted_gateways()
|
self.l3_inst.schedule_unhosted_gateways()
|
||||||
self.nb_idl().update_lrouter_port.assert_not_called()
|
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.'
|
@mock.patch('neutron.services.ovn_l3.plugin.OVNL3RouterPlugin.'
|
||||||
'_get_gateway_port_physnet_mapping')
|
'_get_gateway_port_physnet_mapping')
|
||||||
def test_schedule_unhosted_gateways(self, get_gppm):
|
def test_schedule_unhosted_gateways(self, get_gppm):
|
||||||
@ -1389,7 +1396,7 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
|
|||||||
self.mock_candidates.assert_has_calls([
|
self.mock_candidates.assert_has_calls([
|
||||||
mock.call(mock.ANY,
|
mock.call(mock.ANY,
|
||||||
chassis_physnets=chassis_mappings,
|
chassis_physnets=chassis_mappings,
|
||||||
cms=chassis)] * 3)
|
cms=chassis, availability_zone_hints=[])] * 3)
|
||||||
self.mock_schedule.assert_has_calls([
|
self.mock_schedule.assert_has_calls([
|
||||||
mock.call(self.nb_idl(), self.sb_idl(),
|
mock.call(self.nb_idl(), self.sb_idl(),
|
||||||
'lrp-foo-1', [], ['chassis1', 'chassis2']),
|
'lrp-foo-1', [], ['chassis1', 'chassis2']),
|
||||||
@ -1473,6 +1480,55 @@ class TestOVNL3RouterPlugin(test_mech_driver.Ml2PluginV2TestCase):
|
|||||||
mock.ANY, self.fake_router_port,
|
mock.ANY, self.fake_router_port,
|
||||||
ovn_const.TYPE_ROUTER_PORTS)
|
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,
|
class OVNL3ExtrarouteTests(test_l3_gw.ExtGwModeIntTestCase,
|
||||||
test_l3.L3NatDBIntTestCase,
|
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