From ef2a5543cc7e15769031f81c921d4babb7e14d04 Mon Sep 17 00:00:00 2001 From: Hirofumi Ichihara Date: Thu, 3 Dec 2015 14:12:19 +0900 Subject: [PATCH] Add availability_zone support for router This patch adds the availability_zone support for router. APIImpact DocImpact: Make router scheduler availability zone aware. If multiple availability zones are used, set router_scheduler_driver = neutron.scheduler.l3_agent_scheduler.AZLeastRoutersScheduler. This scheduler selects agent depends on LeastRoutersScheduler logic within an availability zone so that considers the weight of agent. Change-Id: Id26d9494b9a5b459767e93a850f47a3b014b11bb Co-Authored-By: IWAMOTO Toshihiro Partially-implements: blueprint add-availability-zone --- neutron/db/availability_zone/router.py | 43 ++++++ neutron/db/l3_agentschedulers_db.py | 17 +++ neutron/db/l3_attrs_db.py | 2 + neutron/db/l3_hamode_db.py | 7 +- neutron/db/l3_hascheduler_db.py | 9 +- .../alembic_migrations/versions/EXPAND_HEAD | 2 +- .../mitaka/expand/dce3ec7a25c9_router_az.py | 33 +++++ .../extensions/router_availability_zone.py | 68 +++++++++ neutron/scheduler/l3_agent_scheduler.py | 94 +++++++++++- .../services/l3_router/l3_router_plugin.py | 2 +- neutron/tests/unit/extensions/test_l3.py | 3 +- .../test_router_availability_zone.py | 110 ++++++++++++++ .../unit/scheduler/test_l3_agent_scheduler.py | 136 ++++++++++++++++-- ...dd-availability-zone-4440cf00be7c54ba.yaml | 10 +- 14 files changed, 512 insertions(+), 24 deletions(-) create mode 100644 neutron/db/availability_zone/router.py create mode 100644 neutron/db/migration/alembic_migrations/versions/mitaka/expand/dce3ec7a25c9_router_az.py create mode 100644 neutron/extensions/router_availability_zone.py create mode 100644 neutron/tests/unit/extensions/test_router_availability_zone.py diff --git a/neutron/db/availability_zone/router.py b/neutron/db/availability_zone/router.py new file mode 100644 index 000000000..ccb7244f4 --- /dev/null +++ b/neutron/db/availability_zone/router.py @@ -0,0 +1,43 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron.common import utils +from neutron.db import l3_attrs_db +from neutron.extensions import availability_zone as az_ext + + +class RouterAvailabilityZoneMixin(l3_attrs_db.ExtraAttributesMixin): + """Mixin class to enable router's availability zone attributes.""" + + extra_attributes = [{'name': az_ext.AZ_HINTS, 'default': "[]"}] + + def _extend_extra_router_dict(self, router_res, router_db): + super(RouterAvailabilityZoneMixin, self)._extend_extra_router_dict( + router_res, router_db) + if not utils.is_extension_supported(self, 'router_availability_zone'): + return + router_res[az_ext.AZ_HINTS] = az_ext.convert_az_string_to_list( + router_res[az_ext.AZ_HINTS]) + router_res['availability_zones'] = ( + self.get_router_availability_zones(router_db['id'])) + + def _process_extra_attr_router_create( + self, context, router_db, router_req): + if az_ext.AZ_HINTS in router_req: + self.validate_availability_zones(context, 'router', + router_req[az_ext.AZ_HINTS]) + router_req[az_ext.AZ_HINTS] = az_ext.convert_az_list_to_string( + router_req[az_ext.AZ_HINTS]) + super(RouterAvailabilityZoneMixin, + self)._process_extra_attr_router_create(context, router_db, + router_req) diff --git a/neutron/db/l3_agentschedulers_db.py b/neutron/db/l3_agentschedulers_db.py index a7f984410..0258aefff 100644 --- a/neutron/db/l3_agentschedulers_db.py +++ b/neutron/db/l3_agentschedulers_db.py @@ -31,9 +31,11 @@ from neutron.common import utils as n_utils from neutron import context as n_ctx from neutron.db import agents_db from neutron.db import agentschedulers_db +from neutron.db import api as db_api from neutron.db import l3_attrs_db from neutron.db import model_base from neutron.extensions import l3agentscheduler +from neutron.extensions import router_availability_zone as router_az from neutron import manager from neutron.plugins.common import constants as service_constants @@ -546,3 +548,18 @@ class L3AgentSchedulerDbMixin(l3agentscheduler.L3AgentSchedulerPluginBase, RouterL3AgentBinding.l3_agent_id).order_by('count') res = query.filter(agents_db.Agent.id.in_(agent_ids)).first() return res[0] + + +class AZL3AgentSchedulerDbMixin(L3AgentSchedulerDbMixin, + router_az.RouterAvailabilityZonePluginBase): + """Mixin class to add availability_zone supported l3 agent scheduler.""" + + def get_router_availability_zones(self, router_id): + session = db_api.get_session() + with session.begin(): + query = session.query(agents_db.Agent.availability_zone) + query = query.join(RouterL3AgentBinding) + query = query.filter( + RouterL3AgentBinding.router_id == router_id) + query = query.group_by(agents_db.Agent.availability_zone) + return [item[0] for item in query] diff --git a/neutron/db/l3_attrs_db.py b/neutron/db/l3_attrs_db.py index 7c82f84af..2f1b79a6a 100644 --- a/neutron/db/l3_attrs_db.py +++ b/neutron/db/l3_attrs_db.py @@ -44,6 +44,8 @@ class RouterExtraAttributes(model_base.BASEV2): server_default=sa.sql.false(), nullable=False) ha_vr_id = sa.Column(sa.Integer()) + # Availability Zone support + availability_zone_hints = sa.Column(sa.String(255)) router = orm.relationship( l3_db.Router, diff --git a/neutron/db/l3_hamode_db.py b/neutron/db/l3_hamode_db.py index 87545df29..c5b6d09ce 100644 --- a/neutron/db/l3_hamode_db.py +++ b/neutron/db/l3_hamode_db.py @@ -27,6 +27,7 @@ from neutron.common import constants from neutron.common import exceptions as n_exc from neutron.common import utils as n_utils from neutron.db import agents_db +from neutron.db.availability_zone import router as router_az_db from neutron.db import l3_attrs_db from neutron.db import l3_db from neutron.db import l3_dvr_db @@ -136,11 +137,13 @@ class L3HARouterVRIdAllocation(model_base.BASEV2): vr_id = sa.Column(sa.Integer(), nullable=False, primary_key=True) -class L3_HA_NAT_db_mixin(l3_dvr_db.L3_NAT_with_dvr_db_mixin): +class L3_HA_NAT_db_mixin(l3_dvr_db.L3_NAT_with_dvr_db_mixin, + router_az_db.RouterAvailabilityZoneMixin): """Mixin class to add high availability capability to routers.""" extra_attributes = ( - l3_dvr_db.L3_NAT_with_dvr_db_mixin.extra_attributes + [ + l3_dvr_db.L3_NAT_with_dvr_db_mixin.extra_attributes + + router_az_db.RouterAvailabilityZoneMixin.extra_attributes + [ {'name': 'ha', 'default': cfg.CONF.l3_ha}, {'name': 'ha_vr_id', 'default': 0}]) diff --git a/neutron/db/l3_hascheduler_db.py b/neutron/db/l3_hascheduler_db.py index d3a60e366..7acb1c80f 100644 --- a/neutron/db/l3_hascheduler_db.py +++ b/neutron/db/l3_hascheduler_db.py @@ -21,7 +21,7 @@ from neutron.db import l3_attrs_db from neutron.db import l3_db -class L3_HA_scheduler_db_mixin(l3_sch_db.L3AgentSchedulerDbMixin): +class L3_HA_scheduler_db_mixin(l3_sch_db.AZL3AgentSchedulerDbMixin): def get_ha_routers_l3_agents_count(self, context): """Return a map between HA routers and how many agents every @@ -43,10 +43,11 @@ class L3_HA_scheduler_db_mixin(l3_sch_db.L3AgentSchedulerDbMixin): filter(l3_attrs_db.RouterExtraAttributes.ha == sql.true()). group_by(binding_model.router_id).subquery()) - query = (context.session.query( - l3_db.Router.id, l3_db.Router.tenant_id, sub_query.c.count). + query = (context.session.query(l3_db.Router, sub_query.c.count). join(sub_query)) - return query + + return [(self._make_router_dict(router), agent_count) + for router, agent_count in query] def get_l3_agents_ordered_by_num_routers(self, context, agent_ids): if not agent_ids: diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index 2e7ac1ec3..a38beeac2 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -ec7fcfbf72ee +dce3ec7a25c9 diff --git a/neutron/db/migration/alembic_migrations/versions/mitaka/expand/dce3ec7a25c9_router_az.py b/neutron/db/migration/alembic_migrations/versions/mitaka/expand/dce3ec7a25c9_router_az.py new file mode 100644 index 000000000..62af98a06 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/mitaka/expand/dce3ec7a25c9_router_az.py @@ -0,0 +1,33 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Add router availability zone + +Revision ID: dce3ec7a25c9 +Revises: ec7fcfbf72ee +Create Date: 2015-09-17 09:36:17.468901 + +""" + +# revision identifiers, used by Alembic. +revision = 'dce3ec7a25c9' +down_revision = 'ec7fcfbf72ee' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('router_extra_attributes', + sa.Column('availability_zone_hints', sa.String(length=255))) diff --git a/neutron/extensions/router_availability_zone.py b/neutron/extensions/router_availability_zone.py new file mode 100644 index 000000000..f7d2d92bc --- /dev/null +++ b/neutron/extensions/router_availability_zone.py @@ -0,0 +1,68 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc + +import six + +from neutron.api import extensions +from neutron.extensions import availability_zone as az_ext + + +EXTENDED_ATTRIBUTES_2_0 = { + 'routers': { + az_ext.AVAILABILITY_ZONES: {'allow_post': False, 'allow_put': False, + 'is_visible': True}, + az_ext.AZ_HINTS: { + 'allow_post': True, 'allow_put': False, 'is_visible': True, + 'validate': {'type:availability_zone_hints': None}, + 'default': []}} +} + + +class Router_availability_zone(extensions.ExtensionDescriptor): + """Router availability zone extension.""" + + @classmethod + def get_name(cls): + return "Router Availability Zone" + + @classmethod + def get_alias(cls): + return "router_availability_zone" + + @classmethod + def get_description(cls): + return "Availability zone support for router." + + @classmethod + def get_updated(cls): + return "2015-01-01T10:00:00-00:00" + + def get_required_extensions(self): + return ["router", "availability_zone"] + + def get_extended_resources(self, version): + if version == "2.0": + return EXTENDED_ATTRIBUTES_2_0 + else: + return {} + + +@six.add_metaclass(abc.ABCMeta) +class RouterAvailabilityZonePluginBase(object): + + @abc.abstractmethod + def get_router_availability_zones(self, router_id): + """Return availability zones which a router belongs to.""" diff --git a/neutron/scheduler/l3_agent_scheduler.py b/neutron/scheduler/l3_agent_scheduler.py index 29aae7be1..7edc4a306 100644 --- a/neutron/scheduler/l3_agent_scheduler.py +++ b/neutron/scheduler/l3_agent_scheduler.py @@ -14,6 +14,8 @@ # under the License. import abc +import collections +import itertools import random from oslo_config import cfg @@ -28,6 +30,7 @@ from neutron.common import utils from neutron.db import l3_agentschedulers_db from neutron.db import l3_db from neutron.db import l3_hamode_db +from neutron.extensions import availability_zone as az_ext LOG = logging.getLogger(__name__) @@ -298,6 +301,10 @@ class L3Scheduler(object): port_binding.l3_agent_id = agent['id'] self.bind_router(context, router_id, agent) + def get_ha_routers_l3_agents_counts(self, context, plugin, filters=None): + """Return a mapping (router, # agents) matching specified filters.""" + return plugin.get_ha_routers_l3_agents_count(context) + def _schedule_ha_routers_to_additional_agent(self, plugin, context, agent): """Bind already scheduled routers to the agent. @@ -306,18 +313,19 @@ class L3Scheduler(object): is not yet reached. """ - routers_agents = plugin.get_ha_routers_l3_agents_count(context) - + routers_agents = self.get_ha_routers_l3_agents_counts(context, plugin, + agent) scheduled = False admin_ctx = context.elevated() - for router_id, tenant_id, agents in routers_agents: + for router, agents in routers_agents: max_agents_not_reached = ( not self.max_ha_agents or agents < self.max_ha_agents) if max_agents_not_reached: - if not self._router_has_binding(admin_ctx, router_id, + if not self._router_has_binding(admin_ctx, router['id'], agent.id): self.create_ha_port_and_bind(plugin, admin_ctx, - router_id, tenant_id, + router['id'], + router['tenant_id'], agent) scheduled = True @@ -386,3 +394,79 @@ class LeastRoutersScheduler(L3Scheduler): ordered_agents = plugin.get_l3_agents_ordered_by_num_routers( context, [candidate['id'] for candidate in candidates]) return ordered_agents[:num_agents] + + +class AZLeastRoutersScheduler(LeastRoutersScheduler): + """Availability zone aware scheduler. + + If a router is ha router, allocate L3 agents distributed AZs + according to router's az_hints. + """ + def _get_az_hints(self, router): + return (router.get(az_ext.AZ_HINTS) or + cfg.CONF.default_availability_zones) + + def _get_routers_can_schedule(self, context, plugin, routers, l3_agent): + """Overwrite L3Scheduler's method to filter by availability zone.""" + target_routers = [] + for r in routers: + az_hints = self._get_az_hints(r) + if not az_hints or l3_agent['availability_zone'] in az_hints: + target_routers.append(r) + + if not target_routers: + return + + return super(AZLeastRoutersScheduler, self)._get_routers_can_schedule( + context, plugin, target_routers, l3_agent) + + def _get_candidates(self, plugin, context, sync_router): + """Overwrite L3Scheduler's method to filter by availability zone.""" + all_candidates = ( + super(AZLeastRoutersScheduler, self)._get_candidates( + plugin, context, sync_router)) + + candidates = [] + az_hints = self._get_az_hints(sync_router) + for agent in all_candidates: + if not az_hints or agent['availability_zone'] in az_hints: + candidates.append(agent) + + return candidates + + def get_ha_routers_l3_agents_counts(self, context, plugin, filters=None): + """Overwrite L3Scheduler's method to filter by availability zone.""" + all_routers_agents = ( + super(AZLeastRoutersScheduler, self). + get_ha_routers_l3_agents_counts(context, plugin, filters)) + if filters is None: + return all_routers_agents + + routers_agents = [] + for router, agents in all_routers_agents: + az_hints = self._get_az_hints(router) + if az_hints and filters['availability_zone'] not in az_hints: + continue + routers_agents.append((router, agents)) + + return routers_agents + + def _choose_router_agents_for_ha(self, plugin, context, candidates): + ordered_agents = plugin.get_l3_agents_ordered_by_num_routers( + context, [candidate['id'] for candidate in candidates]) + num_agents = self._get_num_of_agents_for_ha(len(ordered_agents)) + + # Order is kept in each az + group_by_az = collections.defaultdict(list) + for agent in ordered_agents: + az = agent['availability_zone'] + group_by_az[az].append(agent) + + selected_agents = [] + for az, agents in itertools.cycle(group_by_az.items()): + if not agents: + continue + selected_agents.append(agents.pop(0)) + if len(selected_agents) >= num_agents: + break + return selected_agents diff --git a/neutron/services/l3_router/l3_router_plugin.py b/neutron/services/l3_router/l3_router_plugin.py index 1579efb55..558d6da13 100644 --- a/neutron/services/l3_router/l3_router_plugin.py +++ b/neutron/services/l3_router/l3_router_plugin.py @@ -53,7 +53,7 @@ class L3RouterPlugin(service_base.ServicePluginBase, """ supported_extension_aliases = ["dvr", "router", "ext-gw-mode", "extraroute", "l3_agent_scheduler", - "l3-ha"] + "l3-ha", "router_availability_zone"] @resource_registry.tracked_resources(router=l3_db.Router, floatingip=l3_db.FloatingIP) diff --git a/neutron/tests/unit/extensions/test_l3.py b/neutron/tests/unit/extensions/test_l3.py index eda6a70d4..ac0e0ef4e 100644 --- a/neutron/tests/unit/extensions/test_l3.py +++ b/neutron/tests/unit/extensions/test_l3.py @@ -330,7 +330,8 @@ class L3NatTestCaseMixin(object): data['router']['name'] = name if admin_state_up: data['router']['admin_state_up'] = admin_state_up - for arg in (('admin_state_up', 'tenant_id') + (arg_list or ())): + for arg in (('admin_state_up', 'tenant_id', 'availability_zone_hints') + + (arg_list or ())): # Arg must be present and not empty if arg in kwargs: data['router'][arg] = kwargs[arg] diff --git a/neutron/tests/unit/extensions/test_router_availability_zone.py b/neutron/tests/unit/extensions/test_router_availability_zone.py new file mode 100644 index 000000000..12e6a8618 --- /dev/null +++ b/neutron/tests/unit/extensions/test_router_availability_zone.py @@ -0,0 +1,110 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six + +from neutron.db.availability_zone import router as router_az_db +from neutron.db import common_db_mixin +from neutron.db import l3_agentschedulers_db +from neutron.db import l3_db +from neutron.extensions import l3 +from neutron.extensions import router_availability_zone as router_az +from neutron.plugins.common import constants as service_constants +from neutron.tests.unit.extensions import test_availability_zone as test_az +from neutron.tests.unit.extensions import test_l3 + + +class AZL3ExtensionManager(test_az.AZExtensionManager): + + def get_resources(self): + return (super(AZL3ExtensionManager, self).get_resources() + + l3.L3.get_resources()) + + +class AZRouterTestPlugin(common_db_mixin.CommonDbMixin, + l3_db.L3_NAT_db_mixin, + router_az_db.RouterAvailabilityZoneMixin, + l3_agentschedulers_db.AZL3AgentSchedulerDbMixin): + supported_extension_aliases = ["router", "l3_agent_scheduler", + "router_availability_zone"] + + def get_plugin_type(self): + return service_constants.L3_ROUTER_NAT + + def get_plugin_description(self): + return "L3 Routing Service Plugin for testing" + + def _create_router_db(self, context, router, tenant_id): + # l3-plugin using routerextraattributes must call + # _process_extra_attr_router_create. + with context.session.begin(subtransactions=True): + router_db = super(AZRouterTestPlugin, self)._create_router_db( + context, router, tenant_id) + self._process_extra_attr_router_create(context, router_db, router) + return router_db + + +class TestAZRouterCase(test_az.AZTestCommon, test_l3.L3NatTestCaseMixin): + def setUp(self): + plugin = ('neutron.tests.unit.extensions.' + 'test_availability_zone.AZTestPlugin') + l3_plugin = ('neutron.tests.unit.extensions.' + 'test_router_availability_zone.AZRouterTestPlugin') + service_plugins = {'l3_plugin_name': l3_plugin} + + self._backup() + l3.RESOURCE_ATTRIBUTE_MAP['routers'].update( + router_az.EXTENDED_ATTRIBUTES_2_0['routers']) + ext_mgr = AZL3ExtensionManager() + super(TestAZRouterCase, self).setUp(plugin=plugin, ext_mgr=ext_mgr, + service_plugins=service_plugins) + + def _backup(self): + self.contents_backup = {} + for res, attrs in six.iteritems(l3.RESOURCE_ATTRIBUTE_MAP): + self.contents_backup[res] = attrs.copy() + self.addCleanup(self._restore) + + def _restore(self): + l3.RESOURCE_ATTRIBUTE_MAP = self.contents_backup + + def test_create_router_with_az(self): + self._register_azs() + az_hints = ['nova2'] + with self.router(availability_zone_hints=az_hints) as router: + res = self._show('routers', router['router']['id']) + self.assertItemsEqual(az_hints, + res['router']['availability_zone_hints']) + + def test_create_router_with_azs(self): + self._register_azs() + az_hints = ['nova2', 'nova3'] + with self.router(availability_zone_hints=az_hints) as router: + res = self._show('routers', router['router']['id']) + self.assertItemsEqual(az_hints, + res['router']['availability_zone_hints']) + + def test_create_router_without_az(self): + with self.router() as router: + res = self._show('routers', router['router']['id']) + self.assertEqual([], res['router']['availability_zone_hints']) + + def test_create_router_with_empty_az(self): + with self.router(availability_zone_hints=[]) as router: + res = self._show('routers', router['router']['id']) + self.assertEqual([], res['router']['availability_zone_hints']) + + def test_create_router_with_none_existing_az(self): + res = self._create_router(self.fmt, 'tenant_id', + availability_zone_hints=['nova4']) + self.assertEqual(404, res.status_int) diff --git a/neutron/tests/unit/scheduler/test_l3_agent_scheduler.py b/neutron/tests/unit/scheduler/test_l3_agent_scheduler.py index 670e5a0a0..aca6bc8b0 100644 --- a/neutron/tests/unit/scheduler/test_l3_agent_scheduler.py +++ b/neutron/tests/unit/scheduler/test_l3_agent_scheduler.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import collections import contextlib import datetime import uuid @@ -1632,7 +1633,7 @@ class L3DvrSchedulerTestCase(testlib_api.SqlTestCase): class L3HAPlugin(db_v2.NeutronDbPluginV2, l3_hamode_db.L3_HA_NAT_db_mixin, l3_hascheduler_db.L3_HA_scheduler_db_mixin): - supported_extension_aliases = ["l3-ha"] + supported_extension_aliases = ["l3-ha", "router_availability_zone"] class L3HATestCaseMixin(testlib_api.SqlTestCase, @@ -1657,11 +1658,14 @@ class L3HATestCaseMixin(testlib_api.SqlTestCase, self._register_l3_agents() - def _create_ha_router(self, ha=True, tenant_id='tenant1'): + def _create_ha_router(self, ha=True, tenant_id='tenant1', az_hints=None): self.adminContext.tenant_id = tenant_id router = {'name': 'router1', 'admin_state_up': True} if ha is not None: router['ha'] = ha + if az_hints is None: + az_hints = [] + router['availability_zone_hints'] = az_hints return self.plugin.create_router(self.adminContext, {'router': router}) @@ -1682,14 +1686,13 @@ class L3_HA_scheduler_db_mixinTestCase(L3HATestCaseMixin): router1 = self._create_ha_router() router2 = self._create_ha_router() router3 = self._create_ha_router(ha=False) - result = self.plugin.get_ha_routers_l3_agents_count( - self.adminContext).all() + result = self.plugin.get_ha_routers_l3_agents_count(self.adminContext) self.assertEqual(2, len(result)) - self.assertIn((router1['id'], router1['tenant_id'], 4), result) - self.assertIn((router2['id'], router2['tenant_id'], 4), result) - self.assertNotIn((router3['id'], router3['tenant_id'], mock.ANY), - result) + check_result = [(router['id'], agents) for router, agents in result] + self.assertIn((router1['id'], 4), check_result) + self.assertIn((router2['id'], 4), check_result) + self.assertNotIn((router3['id'], mock.ANY), check_result) def test_get_ordered_l3_agents_by_num_routers(self): # Mock scheduling so that the test can control it explicitly @@ -2027,3 +2030,120 @@ class TestGetL3AgentsWithAgentModeFilter(testlib_api.SqlTestCase, returned_agent_modes = [self._get_agent_mode(agent) for agent in l3_agents] self.assertEqual(self.expected_agent_modes, returned_agent_modes) + + +class L3AgentAZLeastRoutersSchedulerTestCase(L3HATestCaseMixin): + + def setUp(self): + super(L3AgentAZLeastRoutersSchedulerTestCase, self).setUp() + self.plugin.router_scheduler = importutils.import_object( + 'neutron.scheduler.l3_agent_scheduler.AZLeastRoutersScheduler') + # Mock scheduling so that the test can control it explicitly + mock.patch.object(l3_hamode_db.L3_HA_NAT_db_mixin, + '_notify_ha_interfaces_updated').start() + + def _register_l3_agents(self): + self.agent1 = helpers.register_l3_agent(host='az1-host1', az='az1') + self.agent2 = helpers.register_l3_agent(host='az1-host2', az='az1') + self.agent3 = helpers.register_l3_agent(host='az2-host1', az='az2') + self.agent4 = helpers.register_l3_agent(host='az2-host2', az='az2') + self.agent5 = helpers.register_l3_agent(host='az3-host1', az='az3') + self.agent6 = helpers.register_l3_agent(host='az3-host2', az='az3') + + def test_az_scheduler_auto_schedule(self): + r1 = self._create_ha_router(ha=False, az_hints=['az1']) + self.plugin.auto_schedule_routers(self.adminContext, + 'az1-host2', None) + agents = self.plugin.get_l3_agents_hosting_routers( + self.adminContext, [r1['id']]) + self.assertEqual(1, len(agents)) + self.assertEqual('az1-host2', agents[0]['host']) + + def test_az_scheduler_auto_schedule_no_match(self): + r1 = self._create_ha_router(ha=False, az_hints=['az1']) + self.plugin.auto_schedule_routers(self.adminContext, + 'az2-host1', None) + agents = self.plugin.get_l3_agents_hosting_routers( + self.adminContext, [r1['id']]) + self.assertEqual(0, len(agents)) + + def test_az_scheduler_default_az(self): + cfg.CONF.set_override('default_availability_zones', ['az2']) + r1 = self._create_ha_router(ha=False) + r2 = self._create_ha_router(ha=False) + r3 = self._create_ha_router(ha=False) + self.plugin.schedule_router(self.adminContext, r1['id']) + self.plugin.schedule_router(self.adminContext, r2['id']) + self.plugin.schedule_router(self.adminContext, r3['id']) + agents = self.plugin.get_l3_agents_hosting_routers( + self.adminContext, [r1['id'], r2['id'], r3['id']]) + self.assertEqual(3, len(agents)) + expected_hosts = set(['az2-host1', 'az2-host2']) + hosts = set([a['host'] for a in agents]) + self.assertEqual(expected_hosts, hosts) + + def test_az_scheduler_az_hints(self): + r1 = self._create_ha_router(ha=False, az_hints=['az3']) + r2 = self._create_ha_router(ha=False, az_hints=['az3']) + r3 = self._create_ha_router(ha=False, az_hints=['az3']) + self.plugin.schedule_router(self.adminContext, r1['id']) + self.plugin.schedule_router(self.adminContext, r2['id']) + self.plugin.schedule_router(self.adminContext, r3['id']) + agents = self.plugin.get_l3_agents_hosting_routers( + self.adminContext, [r1['id'], r2['id'], r3['id']]) + self.assertEqual(3, len(agents)) + expected_hosts = set(['az3-host1', 'az3-host2']) + hosts = set([a['host'] for a in agents]) + self.assertEqual(expected_hosts, hosts) + + def test_az_scheduler_least_routers(self): + r1 = self._create_ha_router(ha=False, az_hints=['az1']) + r2 = self._create_ha_router(ha=False, az_hints=['az1']) + r3 = self._create_ha_router(ha=False, az_hints=['az1']) + r4 = self._create_ha_router(ha=False, az_hints=['az1']) + self.plugin.schedule_router(self.adminContext, r1['id']) + self.plugin.schedule_router(self.adminContext, r2['id']) + self.plugin.schedule_router(self.adminContext, r3['id']) + self.plugin.schedule_router(self.adminContext, r4['id']) + agents = self.plugin.get_l3_agents_hosting_routers( + self.adminContext, [r1['id'], r2['id'], r3['id'], r4['id']]) + host_num = collections.defaultdict(int) + for agent in agents: + host_num[agent['host']] += 1 + self.assertEqual(2, host_num['az1-host1']) + self.assertEqual(2, host_num['az1-host2']) + + def test_az_scheduler_ha_az_hints(self): + cfg.CONF.set_override('max_l3_agents_per_router', 2) + r1 = self._create_ha_router(az_hints=['az1', 'az3']) + self.plugin.schedule_router(self.adminContext, r1['id']) + agents = self.plugin.get_l3_agents_hosting_routers( + self.adminContext, [r1['id']]) + self.assertEqual(2, len(agents)) + expected_azs = set(['az1', 'az3']) + azs = set([a['availability_zone'] for a in agents]) + self.assertEqual(expected_azs, azs) + + def test_az_scheduler_ha_auto_schedule(self): + cfg.CONF.set_override('max_l3_agents_per_router', 3) + r1 = self._create_ha_router(az_hints=['az1', 'az3']) + self._set_l3_agent_admin_state(self.adminContext, self.agent2['id'], + state=False) + self._set_l3_agent_admin_state(self.adminContext, self.agent6['id'], + state=False) + self.plugin.schedule_router(self.adminContext, r1['id']) + agents = self.plugin.get_l3_agents_hosting_routers( + self.adminContext, [r1['id']]) + self.assertEqual(2, len(agents)) + hosts = set([a['host'] for a in agents]) + self.assertEqual(set(['az1-host1', 'az3-host1']), hosts) + self._set_l3_agent_admin_state(self.adminContext, self.agent6['id'], + state=True) + self.plugin.auto_schedule_routers(self.adminContext, + 'az3-host2', None) + agents = self.plugin.get_l3_agents_hosting_routers( + self.adminContext, [r1['id']]) + self.assertEqual(3, len(agents)) + expected_hosts = set(['az1-host1', 'az3-host1', 'az3-host2']) + hosts = set([a['host'] for a in agents]) + self.assertEqual(expected_hosts, hosts) diff --git a/releasenotes/notes/add-availability-zone-4440cf00be7c54ba.yaml b/releasenotes/notes/add-availability-zone-4440cf00be7c54ba.yaml index 0a873f1f2..6a4a0e34b 100644 --- a/releasenotes/notes/add-availability-zone-4440cf00be7c54ba.yaml +++ b/releasenotes/notes/add-availability-zone-4440cf00be7c54ba.yaml @@ -1,4 +1,10 @@ --- +prelude: > + Agent scheduling aware of availability zones is now supported. features: - - DHCP agent is assigned to a availability zone. Network can be host on the - DHCP agent with availability zone which users specify. \ No newline at end of file + - DHCP agent is assigned to an availability zone; network will be hosted by + the DHCP agent with availability zone which user specifies. + - L3 agent is assigned to an availability zone; router will be hosted by the + L3 agent with availability zone which user specifies. This supports the use + of availability zones with HA routers. DVR isn't supported now because L3HA + and DVR integration isn't finished. \ No newline at end of file -- 2.45.2