]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Add availability_zone support for router
authorHirofumi Ichihara <ichihara.hirofumi@lab.ntt.co.jp>
Thu, 3 Dec 2015 05:12:19 +0000 (14:12 +0900)
committerHirofumi Ichihara <ichihara.hirofumi@lab.ntt.co.jp>
Fri, 4 Dec 2015 03:32:42 +0000 (12:32 +0900)
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 <iwamoto@valinux.co.jp>
Partially-implements: blueprint add-availability-zone

14 files changed:
neutron/db/availability_zone/router.py [new file with mode: 0644]
neutron/db/l3_agentschedulers_db.py
neutron/db/l3_attrs_db.py
neutron/db/l3_hamode_db.py
neutron/db/l3_hascheduler_db.py
neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD
neutron/db/migration/alembic_migrations/versions/mitaka/expand/dce3ec7a25c9_router_az.py [new file with mode: 0644]
neutron/extensions/router_availability_zone.py [new file with mode: 0644]
neutron/scheduler/l3_agent_scheduler.py
neutron/services/l3_router/l3_router_plugin.py
neutron/tests/unit/extensions/test_l3.py
neutron/tests/unit/extensions/test_router_availability_zone.py [new file with mode: 0644]
neutron/tests/unit/scheduler/test_l3_agent_scheduler.py
releasenotes/notes/add-availability-zone-4440cf00be7c54ba.yaml

diff --git a/neutron/db/availability_zone/router.py b/neutron/db/availability_zone/router.py
new file mode 100644 (file)
index 0000000..ccb7244
--- /dev/null
@@ -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)
index a7f984410abbaee5b94e39529bdebd1cacad6711..0258aefff2186aa50d4502c99a5102a090e902ea 100644 (file)
@@ -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]
index 7c82f84af1e8f25531c18e44ed96d0f061d6db79..2f1b79a6ab80a9fde9d967e1f19d8a20bf3ea95d 100644 (file)
@@ -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,
index 87545df2901e5b3d14bf34cfdc9490b583ac9ee6..c5b6d09ce9aeb3a335de80e9c8bb023aec1a883c 100644 (file)
@@ -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}])
 
index d3a60e36658573fe3d78c950ebc6bab3263e7ddb..7acb1c80f72b209c85249bf69074df618989ba86 100644 (file)
@@ -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:
index 2e7ac1ec3ca0aacaef0de2e9f84beed50f98c24c..a38beeac26caec5cd82f6a139c5de25fdc3e72dd 100644 (file)
@@ -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 (file)
index 0000000..62af98a
--- /dev/null
@@ -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 (file)
index 0000000..f7d2d92
--- /dev/null
@@ -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."""
index 29aae7be1e403a7365630fc1449ccb50216054ef..7edc4a306d7873368dfcb66822e592fa3ba20e6b 100644 (file)
@@ -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
index 1579efb55cc40810e709aa3308f123af9971ec02..558d6da137f714d8c94b0db5cde6a9f5f047660e 100644 (file)
@@ -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)
index eda6a70d416b2aa10106a7ceb3b73b49a31702a0..ac0e0ef4eb99a18af2e792eae32744e77a0a3887 100644 (file)
@@ -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 (file)
index 0000000..12e6a86
--- /dev/null
@@ -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)
index 670e5a0a0ceff2ae23f3b0959df5a158049f48fa..aca6bc8b0da5bca192a12d7bfdba0ef64c9dada9 100644 (file)
@@ -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)
index 0a873f1f21dc92f998aa4cf3545a72408e6a1b71..6a4a0e34b7d0ab570e1be36c13a79405ad8791b9 100644 (file)
@@ -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