]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Add availability_zone support base
authorHirofumi Ichihara <ichihara.hirofumi@lab.ntt.co.jp>
Fri, 2 Oct 2015 01:14:10 +0000 (10:14 +0900)
committerHirofumi Ichihara <ichihara.hirofumi@lab.ntt.co.jp>
Fri, 2 Oct 2015 01:14:10 +0000 (10:14 +0900)
This patch adds the availability_zone attribute to agents and
supports availability_zone API.
Availability_zone support for resources (network/router) and
the schedulers are included in subsequent patches.

APIImpact
DocImpact

Co-Authored-By: IWAMOTO Toshihiro <iwamoto@valinux.co.jp>
Change-Id: Id7a62000ab0484412b3970199df8c374568fe70d
Partially-implements: blueprint add-availability-zone

14 files changed:
etc/neutron.conf
neutron/agent/common/config.py
neutron/agent/dhcp/agent.py
neutron/agent/dhcp_agent.py
neutron/agent/l3/agent.py
neutron/agent/l3_agent.py
neutron/db/agents_db.py
neutron/db/migration/alembic_migrations/versions/mitaka/expand/59cb5b6cf4d_availability_zone.py [new file with mode: 0644]
neutron/extensions/agent.py
neutron/extensions/availability_zone.py [new file with mode: 0644]
neutron/plugins/ml2/plugin.py
neutron/tests/common/helpers.py
neutron/tests/unit/agent/l3/test_agent.py
neutron/tests/unit/extensions/test_availability_zone.py [new file with mode: 0644]

index 77f8d92f4d65b17433c7cdb8da53b1c947a486f2..dc8894860593f2b246b3197a42d3885d02822f31 100644 (file)
 #   exit - Exits the agent
 # check_child_processes_action = respawn
 
+# Availability zone of this node.
+# availability_zone = nova
+
 # =========== items for agent management extension =============
 # seconds between nodes reporting state to server; should be less than
 # agent_down_time, best if it is half or less than agent_down_time
index 100ef34edb29f5af657ec8980e0f7d05d05e0671..6235c23b0881eb26a72faebd64e2c88134c64b43 100644 (file)
@@ -16,6 +16,7 @@
 import os
 
 from oslo_config import cfg
+from oslo_config import types
 from oslo_log import log as logging
 
 from neutron.common import config
@@ -75,6 +76,37 @@ PROCESS_MONITOR_OPTS = [
 ]
 
 
+# TODO(hichihara): Remove these two classes, once oslo fixes types.string
+# and cfg.StrOpt.
+class LengthString(types.String):
+    def __init__(self, maxlen=None):
+        super(LengthString, self).__init__()
+        self.maxlen = maxlen
+
+    def __call__(self, value):
+        value = super(LengthString, self).__call__(value)
+        if self.maxlen and len(value) > self.maxlen:
+            raise ValueError(_("String value '%(value)s' exceeds max length "
+                               "%(len)d") % {'value': value,
+                                             'len': self.maxlen})
+        return value
+
+
+class LengthStrOpt(cfg.Opt):
+    def __init__(self, name, maxlen=None, **kwargs):
+        super(LengthStrOpt, self).__init__(name,
+                                           type=LengthString(maxlen=maxlen),
+                                           **kwargs)
+
+
+AVAILABILITY_ZONE_OPTS = [
+    # The default AZ name "nova" is selected to match the default
+    # AZ name in Nova and Cinder.
+    LengthStrOpt('availability_zone', maxlen=255, default='nova',
+                 help=_("Availability zone of this node")),
+]
+
+
 def get_log_args(conf, log_file_name, **kwargs):
     cmd_args = []
     if conf.debug:
@@ -128,6 +160,10 @@ def register_process_monitor_opts(conf):
     conf.register_opts(PROCESS_MONITOR_OPTS, 'AGENT')
 
 
+def register_availability_zone_opts_helper(conf):
+    conf.register_opts(AVAILABILITY_ZONE_OPTS, 'AGENT')
+
+
 def get_root_helper(conf):
     return conf.AGENT.root_helper
 
index 4e4da5036ae3ef63f982b688f4a57332461ef6ab..f077d4fe129dc57d25399ed27e192639575c23e8 100644 (file)
@@ -548,6 +548,7 @@ class DhcpAgentWithStateReport(DhcpAgent):
         self.agent_state = {
             'binary': 'neutron-dhcp-agent',
             'host': host,
+            'availability_zone': self.conf.AGENT.availability_zone,
             'topic': topics.DHCP_AGENT,
             'configurations': {
                 'dhcp_driver': self.conf.dhcp_driver,
index 968a3d1c5b28e6f9cbfaf3c971c347e3d91dd9b4..634b3b6d3c93f4e66a423dd9ffa58666365c45c6 100644 (file)
@@ -32,6 +32,7 @@ def register_options(conf):
     config.register_interface_driver_opts_helper(conf)
     config.register_use_namespaces_opts_helper(conf)
     config.register_agent_state_opts_helper(conf)
+    config.register_availability_zone_opts_helper(conf)
     conf.register_opts(dhcp_config.DHCP_AGENT_OPTS)
     conf.register_opts(dhcp_config.DHCP_OPTS)
     conf.register_opts(dhcp_config.DNSMASQ_OPTS)
index 6f2b9077253e8ec250e06dbfa5094e898ce6347d..570a6c420f6a2e5f0494a9a7d74f6d574dadefb4 100644 (file)
@@ -615,6 +615,7 @@ class L3NATAgentWithStateReport(L3NATAgent):
         self.agent_state = {
             'binary': 'neutron-l3-agent',
             'host': host,
+            'availability_zone': self.conf.AGENT.availability_zone,
             'topic': topics.L3_AGENT,
             'configurations': {
                 'agent_mode': self.conf.agent_mode,
index bee060181c978c9b9d47417f5ae50ad3f68fced5..8c34fe1ac440262f162aad6b08491dc9d62151ec 100644 (file)
@@ -40,6 +40,7 @@ def register_opts(conf):
     config.register_agent_state_opts_helper(conf)
     conf.register_opts(interface.OPTS)
     conf.register_opts(external_process.OPTS)
+    config.register_availability_zone_opts_helper(conf)
 
 
 def main(manager='neutron.agent.l3.agent.L3NATAgentWithStateReport'):
index 9417d5e3c37d407f35cb406dea9c1d7105b140ec..c704f884b84a3ac9a7360de6e250f4c49f54b88b 100644 (file)
@@ -20,6 +20,7 @@ from oslo_log import log as logging
 import oslo_messaging
 from oslo_serialization import jsonutils
 from oslo_utils import timeutils
+import six
 import sqlalchemy as sa
 from sqlalchemy.orm import exc
 from sqlalchemy import sql
@@ -29,6 +30,7 @@ from neutron.common import constants
 from neutron.db import model_base
 from neutron.db import models_v2
 from neutron.extensions import agent as ext_agent
+from neutron.extensions import availability_zone as az_ext
 from neutron.i18n import _LE, _LI, _LW
 from neutron import manager
 
@@ -81,6 +83,7 @@ class Agent(model_base.BASEV2, models_v2.HasId):
     topic = sa.Column(sa.String(255), nullable=False)
     # TOPIC.host is a target topic
     host = sa.Column(sa.String(255), nullable=False)
+    availability_zone = sa.Column(sa.String(255))
     admin_state_up = sa.Column(sa.Boolean, default=True,
                                server_default=sql.true(), nullable=False)
     # the time when first report came from agents
@@ -101,7 +104,60 @@ class Agent(model_base.BASEV2, models_v2.HasId):
         return not AgentDbMixin.is_agent_down(self.heartbeat_timestamp)
 
 
-class AgentDbMixin(ext_agent.AgentPluginBase):
+class AgentAvailabilityZoneMixin(az_ext.AvailabilityZonePluginBase):
+    """Mixin class to add availability_zone extension to AgentDbMixin."""
+
+    def _list_availability_zones(self, context, filters=None):
+        result = {}
+        query = self._get_collection_query(context, Agent, filters=filters)
+        for agent in query.group_by(Agent.admin_state_up,
+                                    Agent.availability_zone,
+                                    Agent.agent_type):
+            if not agent.availability_zone:
+                continue
+            if agent.agent_type == constants.AGENT_TYPE_DHCP:
+                resource = 'network'
+            elif agent.agent_type == constants.AGENT_TYPE_L3:
+                resource = 'router'
+            else:
+                continue
+            key = (agent.availability_zone, resource)
+            result[key] = agent.admin_state_up or result.get(key, False)
+        return result
+
+    def get_availability_zones(self, context, filters=None, fields=None,
+                               sorts=None, limit=None, marker=None,
+                               page_reverse=False):
+        """Return a list of availability zones."""
+        # NOTE(hichihara): 'tenant_id' is dummy for policy check.
+        # it is not visible via API.
+        return [{'state': 'available' if v else 'unavailable',
+                 'name': k[0], 'resource': k[1],
+                 'tenant_id': context.tenant_id}
+                for k, v in six.iteritems(self._list_availability_zones(
+                                           context, filters))]
+
+    def validate_availability_zones(self, context, resource_type,
+                                    availability_zones):
+        """Verify that the availability zones exist."""
+        if not availability_zones:
+            return
+        if resource_type == 'network':
+            agent_type = constants.AGENT_TYPE_DHCP
+        elif resource_type == 'router':
+            agent_type = constants.AGENT_TYPE_L3
+        else:
+            return
+        query = context.session.query(Agent.availability_zone).filter_by(
+                    agent_type=agent_type).group_by(Agent.availability_zone)
+        query = query.filter(Agent.availability_zone.in_(availability_zones))
+        azs = [item[0] for item in query]
+        diff = set(availability_zones) - set(azs)
+        if diff:
+            raise az_ext.AvailabilityZoneNotFound(availability_zone=diff.pop())
+
+
+class AgentDbMixin(ext_agent.AgentPluginBase, AgentAvailabilityZoneMixin):
     """Mixin class to add agent extension to db_base_plugin_v2."""
 
     def _get_agent(self, context, id):
@@ -162,6 +218,7 @@ class AgentDbMixin(ext_agent.AgentPluginBase):
         res['alive'] = not AgentDbMixin.is_agent_down(
             res['heartbeat_timestamp'])
         res['configurations'] = self.get_configuration_dict(agent)
+        res['availability_zone'] = agent['availability_zone']
         return self._fields(res, fields)
 
     def delete_agent(self, context, id):
@@ -222,7 +279,8 @@ class AgentDbMixin(ext_agent.AgentPluginBase):
         with context.session.begin(subtransactions=True):
             res_keys = ['agent_type', 'binary', 'host', 'topic']
             res = dict((k, agent_state[k]) for k in res_keys)
-
+            if 'availability_zone' in agent_state:
+                res['availability_zone'] = agent_state['availability_zone']
             configurations_dict = agent_state.get('configurations', {})
             res['configurations'] = jsonutils.dumps(configurations_dict)
             res['load'] = self._get_agent_load(agent_state)
diff --git a/neutron/db/migration/alembic_migrations/versions/mitaka/expand/59cb5b6cf4d_availability_zone.py b/neutron/db/migration/alembic_migrations/versions/mitaka/expand/59cb5b6cf4d_availability_zone.py
new file mode 100644 (file)
index 0000000..d2c5482
--- /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 availability zone
+
+Revision ID: 59cb5b6cf4d
+Revises: 34af2b5c5a59
+Create Date: 2015-01-20 14:38:47.156574
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '59cb5b6cf4d'
+down_revision = '34af2b5c5a59'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+    op.add_column('agents',
+                  sa.Column('availability_zone', sa.String(length=255)))
index c8e40a7c6c56cead7116315579724062ff19d587..c83d3fb4987ac46f4c699898a0b7670b5adf6285 100644 (file)
@@ -108,6 +108,10 @@ class Agent(extensions.ExtensionDescriptor):
 
         return [ex]
 
+    def update_attributes_map(self, attributes):
+        super(Agent, self).update_attributes_map(
+            attributes, extension_attrs_map=RESOURCE_ATTRIBUTE_MAP)
+
     def get_extended_resources(self, version):
         if version == "2.0":
             return RESOURCE_ATTRIBUTE_MAP
diff --git a/neutron/extensions/availability_zone.py b/neutron/extensions/availability_zone.py
new file mode 100644 (file)
index 0000000..2c06c18
--- /dev/null
@@ -0,0 +1,107 @@
+#
+# 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
+
+from neutron.api import extensions
+from neutron.api.v2 import attributes as attr
+from neutron.api.v2 import base
+from neutron.common import exceptions
+from neutron import manager
+
+
+# Attribute Map
+RESOURCE_NAME = 'availability_zone'
+AVAILABILITY_ZONES = 'availability_zones'
+# name: name of availability zone (string)
+# resource: type of resource: 'network' or 'router'
+# state: state of availability zone: 'available' or 'unavailable'
+# It means whether users can use the availability zone.
+RESOURCE_ATTRIBUTE_MAP = {
+    AVAILABILITY_ZONES: {
+        'name': {'is_visible': True},
+        'resource': {'is_visible': True},
+        'state': {'is_visible': True}
+    }
+}
+
+EXTENDED_ATTRIBUTES_2_0 = {
+    'agents': {
+        RESOURCE_NAME: {'allow_post': False, 'allow_put': False,
+                        'is_visible': True}
+    }
+}
+
+
+class AvailabilityZoneNotFound(exceptions.NotFound):
+    message = _("AvailabilityZone %(availability_zone)s could not be found.")
+
+
+class Availability_zone(extensions.ExtensionDescriptor):
+    """Availability zone extension."""
+
+    @classmethod
+    def get_name(cls):
+        return "Availability Zone"
+
+    @classmethod
+    def get_alias(cls):
+        return "availability_zone"
+
+    @classmethod
+    def get_description(cls):
+        return "The availability zone extension."
+
+    @classmethod
+    def get_updated(cls):
+        return "2015-01-01T10:00:00-00:00"
+
+    def get_required_extensions(self):
+        return ["agent"]
+
+    @classmethod
+    def get_resources(cls):
+        """Returns Ext Resources."""
+        my_plurals = [(key, key[:-1]) for key in RESOURCE_ATTRIBUTE_MAP.keys()]
+        attr.PLURALS.update(dict(my_plurals))
+        plugin = manager.NeutronManager.get_plugin()
+        params = RESOURCE_ATTRIBUTE_MAP.get(AVAILABILITY_ZONES)
+        controller = base.create_resource(AVAILABILITY_ZONES,
+                                          RESOURCE_NAME, plugin, params)
+
+        ex = extensions.ResourceExtension(AVAILABILITY_ZONES, controller)
+
+        return [ex]
+
+    def get_extended_resources(self, version):
+        if version == "2.0":
+            return dict(list(EXTENDED_ATTRIBUTES_2_0.items()) +
+                        list(RESOURCE_ATTRIBUTE_MAP.items()))
+        else:
+            return {}
+
+
+class AvailabilityZonePluginBase(object):
+    """REST API to operate the Availability Zone."""
+
+    @abc.abstractmethod
+    def get_availability_zones(self, context, filters=None, fields=None,
+                               sorts=None, limit=None, marker=None,
+                               page_reverse=False):
+        pass
+
+    @abc.abstractmethod
+    def validate_availability_zones(self, context, resource_type,
+                                    availability_zones):
+        pass
index d6f98f26cf6d4ae7ab7e2d6e86f7521cf8c9e38b..3d2cbdeaf53854f1bd6e66177fe8d286f711a494 100644 (file)
@@ -118,7 +118,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
                                     "multi-provider", "allowed-address-pairs",
                                     "extra_dhcp_opt", "subnet_allocation",
                                     "net-mtu", "vlan-transparent",
-                                    "address-scope", "dns-integration"]
+                                    "address-scope", "dns-integration",
+                                    "availability_zone"]
 
     @property
     def supported_extension_aliases(self):
index 80473cf56a8884e18ec1382436aa2f42646abefb..484c0216580e015efe6ea3eb3740d69eecb3550a 100644 (file)
@@ -25,6 +25,7 @@ from neutron.db import agents_db
 from neutron.db import common_db_mixin
 
 HOST = 'localhost'
+DEFAULT_AZ = 'nova'
 
 
 def find_file(filename, path):
@@ -47,12 +48,14 @@ class FakePlugin(common_db_mixin.CommonDbMixin,
 
 
 def _get_l3_agent_dict(host, agent_mode, internal_only=True,
-                       ext_net_id='', ext_bridge='', router_id=None):
+                       ext_net_id='', ext_bridge='', router_id=None,
+                       az=DEFAULT_AZ):
     return {
         'agent_type': constants.AGENT_TYPE_L3,
         'binary': 'neutron-l3-agent',
         'host': host,
         'topic': topics.L3_AGENT,
+        'availability_zone': az,
         'configurations': {'agent_mode': agent_mode,
                            'handle_internal_only_routers': internal_only,
                            'external_network_bridge': ext_bridge,
@@ -71,18 +74,19 @@ def _register_agent(agent):
 
 def register_l3_agent(host=HOST, agent_mode=constants.L3_AGENT_MODE_LEGACY,
                       internal_only=True, ext_net_id='', ext_bridge='',
-                      router_id=None):
+                      router_id=None, az=DEFAULT_AZ):
     agent = _get_l3_agent_dict(host, agent_mode, internal_only, ext_net_id,
-                               ext_bridge, router_id)
+                               ext_bridge, router_id, az)
     return _register_agent(agent)
 
 
-def _get_dhcp_agent_dict(host, networks=0):
+def _get_dhcp_agent_dict(host, networks=0, az=DEFAULT_AZ):
     agent = {
         'binary': 'neutron-dhcp-agent',
         'host': host,
         'topic': topics.DHCP_AGENT,
         'agent_type': constants.AGENT_TYPE_DHCP,
+        'availability_zone': az,
         'configurations': {'dhcp_driver': 'dhcp_driver',
                            'use_namespaces': True,
                            'networks': networks}}
@@ -90,9 +94,9 @@ def _get_dhcp_agent_dict(host, networks=0):
 
 
 def register_dhcp_agent(host=HOST, networks=0, admin_state_up=True,
-                        alive=True):
+                        alive=True, az=DEFAULT_AZ):
     agent = _register_agent(
-        _get_dhcp_agent_dict(host, networks))
+        _get_dhcp_agent_dict(host, networks, az=az))
 
     if not admin_state_up:
         set_agent_admin_state(agent['id'])
index 3be71f5c07ede8bd44fe030bed39b460aec75c59..ea9409917c58b5f6b8bc4f20e93a52e854fb011e 100644 (file)
@@ -70,6 +70,7 @@ class BasicRouterOperationsFramework(base.BaseTestCase):
         agent_config.register_interface_driver_opts_helper(self.conf)
         agent_config.register_use_namespaces_opts_helper(self.conf)
         agent_config.register_process_monitor_opts(self.conf)
+        agent_config.register_availability_zone_opts_helper(self.conf)
         self.conf.register_opts(interface.OPTS)
         self.conf.register_opts(external_process.OPTS)
         self.conf.set_override('router_id', 'fake_id')
diff --git a/neutron/tests/unit/extensions/test_availability_zone.py b/neutron/tests/unit/extensions/test_availability_zone.py
new file mode 100644 (file)
index 0000000..d68ea90
--- /dev/null
@@ -0,0 +1,98 @@
+#
+#    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 oslo_log import log as logging
+
+from neutron import context
+from neutron.db import agents_db
+from neutron.db import db_base_plugin_v2
+from neutron.extensions import agent
+from neutron.extensions import availability_zone as az_ext
+from neutron.tests.common import helpers
+from neutron.tests.unit.db import test_db_base_plugin_v2
+
+
+LOG = logging.getLogger(__name__)
+
+
+class AZExtensionManager(object):
+
+    def get_resources(self):
+        agent.RESOURCE_ATTRIBUTE_MAP['agents'].update(
+            az_ext.EXTENDED_ATTRIBUTES_2_0['agents'])
+        return (az_ext.Availability_zone.get_resources() +
+                agent.Agent.get_resources())
+
+    def get_actions(self):
+        return []
+
+    def get_request_extensions(self):
+        return []
+
+
+# This plugin class is just for testing
+class AZTestPlugin(db_base_plugin_v2.NeutronDbPluginV2,
+                   agents_db.AgentDbMixin):
+    supported_extension_aliases = ["agent", "availability_zone"]
+
+
+class AZTestCommon(test_db_base_plugin_v2.NeutronDbPluginV2TestCase):
+    def _register_azs(self):
+        self.agent1 = helpers.register_dhcp_agent(host='host1', az='nova1')
+        self.agent2 = helpers.register_dhcp_agent(host='host2', az='nova2')
+        self.agent3 = helpers.register_l3_agent(host='host2', az='nova2')
+        self.agent4 = helpers.register_l3_agent(host='host3', az='nova3')
+        self.agent5 = helpers.register_l3_agent(host='host4', az='nova2')
+
+
+class TestAZAgentCase(AZTestCommon):
+    def setUp(self):
+        plugin = ('neutron.tests.unit.extensions.'
+                  'test_availability_zone.AZTestPlugin')
+        ext_mgr = AZExtensionManager()
+        super(TestAZAgentCase, self).setUp(plugin=plugin, ext_mgr=ext_mgr)
+
+    def test_list_availability_zones(self):
+        self._register_azs()
+        helpers.set_agent_admin_state(self.agent3['id'], admin_state_up=False)
+        helpers.set_agent_admin_state(self.agent4['id'], admin_state_up=False)
+        expected = [
+            {'name': 'nova1', 'resource': 'network', 'state': 'available'},
+            {'name': 'nova2', 'resource': 'network', 'state': 'available'},
+            {'name': 'nova2', 'resource': 'router', 'state': 'available'},
+            {'name': 'nova3', 'resource': 'router', 'state': 'unavailable'}]
+        res = self._list('availability_zones')
+        azs = res['availability_zones']
+        self.assertItemsEqual(expected, azs)
+        # not admin case
+        ctx = context.Context('', 'noadmin')
+        res = self._list('availability_zones', neutron_context=ctx)
+        azs = res['availability_zones']
+        self.assertItemsEqual(expected, azs)
+
+    def test_list_agent_with_az(self):
+        helpers.register_dhcp_agent(host='host1', az='nova1')
+        res = self._list('agents')
+        self.assertEqual('nova1',
+            res['agents'][0]['availability_zone'])
+
+    def test_validate_availability_zones(self):
+        self._register_azs()
+        ctx = context.Context('', 'tenant_id')
+        self.plugin.validate_availability_zones(ctx, 'network',
+                                                ['nova1', 'nova2'])
+        self.plugin.validate_availability_zones(ctx, 'router',
+                                                ['nova2', 'nova3'])
+        self.assertRaises(az_ext.AvailabilityZoneNotFound,
+                          self.plugin.validate_availability_zones,
+                          ctx, 'router', ['nova1'])