]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add os-availability-zone extension
authorBrian Waldon <bcwaldon@gmail.com>
Wed, 26 Jun 2013 16:43:48 +0000 (09:43 -0700)
committerBrian Waldon <bcwaldon@gmail.com>
Fri, 28 Jun 2013 14:26:34 +0000 (07:26 -0700)
* Query /os-availability-zone to get an object representing the configured
  availability zones and their state
* This implements a subset of Nova's os-availability-zone extension

Fixes bug 1195461

Change-Id: Ic0a8eb5a82ca0a4eed3b1e1cd6cf3a4665589307

cinder/api/contrib/availability_zones.py [new file with mode: 0644]
cinder/api/views/availability_zones.py [new file with mode: 0644]
cinder/tests/api/contrib/test_availability_zones.py [new file with mode: 0644]
cinder/tests/api/v1/stubs.py
cinder/tests/api/v2/stubs.py
cinder/tests/test_volume.py
cinder/volume/api.py

diff --git a/cinder/api/contrib/availability_zones.py b/cinder/api/contrib/availability_zones.py
new file mode 100644 (file)
index 0000000..fd2344c
--- /dev/null
@@ -0,0 +1,70 @@
+# Copyright (c) 2013 OpenStack Foundation
+# All Rights Reserved.
+#
+#    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 cinder.api import extensions
+from cinder.api.openstack import wsgi
+import cinder.api.views.availability_zones
+from cinder.api import xmlutil
+import cinder.exception
+import cinder.volume.api
+
+
+def make_availability_zone(elem):
+    elem.set('name', 'zoneName')
+    zoneStateElem = xmlutil.SubTemplateElement(elem, 'zoneState',
+                                               selector='zoneState')
+    zoneStateElem.set('available')
+
+
+class ListTemplate(xmlutil.TemplateBuilder):
+    def construct(self):
+        root = xmlutil.TemplateElement('availabilityZones')
+        elem = xmlutil.SubTemplateElement(root, 'availabilityZone',
+                                          selector='availabilityZoneInfo')
+        make_availability_zone(elem)
+        alias = Availability_zones.alias
+        namespace = Availability_zones.namespace
+        return xmlutil.MasterTemplate(root, 1, nsmap={alias: namespace})
+
+
+class Controller(wsgi.Controller):
+
+    _view_builder_class = cinder.api.views.availability_zones.ViewBuilder
+
+    def __init__(self, *args, **kwargs):
+        super(Controller, self).__init__(*args, **kwargs)
+        self.volume_api = cinder.volume.api.API()
+
+    @wsgi.serializers(xml=ListTemplate)
+    def index(self, req):
+        """Describe all known availability zones."""
+        azs = self.volume_api.list_availability_zones()
+        return self._view_builder.list(req, azs)
+
+
+class Availability_zones(extensions.ExtensionDescriptor):
+    """Describe Availability Zones"""
+
+    name = 'AvailabilityZones'
+    alias = 'os-availability-zone'
+    namespace = ('http://docs.openstack.org/volume/ext/'
+                 'os-availability-zone/api/v1')
+    updated = '2013-06-27T00:00:00+00:00'
+
+    def get_resources(self):
+        controller = Controller()
+        res = extensions.ResourceExtension(Availability_zones.alias,
+                                           controller)
+        return [res]
diff --git a/cinder/api/views/availability_zones.py b/cinder/api/views/availability_zones.py
new file mode 100644 (file)
index 0000000..5031cc2
--- /dev/null
@@ -0,0 +1,29 @@
+# Copyright (c) 2013 OpenStack Foundation
+# All Rights Reserved.
+#
+#    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 cinder.api.common
+
+
+class ViewBuilder(cinder.api.common.ViewBuilder):
+    """Map cinder.volumes.api list_availability_zones response into dicts"""
+
+    def list(self, request, availability_zones):
+        def fmt(az):
+            return {
+                'zoneName': az['name'],
+                'zoneState': {'available': az['available']},
+            }
+
+        return {'availabilityZoneInfo': [fmt(az) for az in availability_zones]}
diff --git a/cinder/tests/api/contrib/test_availability_zones.py b/cinder/tests/api/contrib/test_availability_zones.py
new file mode 100644 (file)
index 0000000..1c2bed8
--- /dev/null
@@ -0,0 +1,90 @@
+# Copyright (c) 2013 OpenStack Foundation
+# All Rights Reserved.
+#
+#    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 datetime
+
+from lxml import etree
+
+import cinder.api.contrib.availability_zones
+import cinder.context
+from cinder.openstack.common import timeutils
+import cinder.test
+import cinder.volume.api
+
+
+created_time = datetime.datetime(2012, 11, 14, 1, 20, 41, 95099)
+current_time = timeutils.utcnow()
+
+
+def list_availability_zones(self):
+    return (
+        {'name': 'ping', 'available': True},
+        {'name': 'pong', 'available': False},
+    )
+
+
+class FakeRequest(object):
+    environ = {'cinder.context': cinder.context.get_admin_context()}
+    GET = {}
+
+
+class ControllerTestCase(cinder.test.TestCase):
+
+    def setUp(self):
+        super(ControllerTestCase, self).setUp()
+        self.controller = cinder.api.contrib.availability_zones.Controller()
+        self.req = FakeRequest()
+        self.stubs.Set(cinder.volume.api.API,
+                       'list_availability_zones',
+                       list_availability_zones)
+
+    def test_list_hosts(self):
+        """Verify that the volume hosts are returned."""
+        actual = self.controller.index(self.req)
+        expected = {
+            'availabilityZoneInfo': [
+                {'zoneName': 'ping', 'zoneState': {'available': True}},
+                {'zoneName': 'pong', 'zoneState': {'available': False}},
+            ],
+        }
+        self.assertEqual(expected, actual)
+
+
+class XMLSerializerTest(cinder.test.TestCase):
+
+    def test_index_xml(self):
+        fixture = {
+            'availabilityZoneInfo': [
+                {'zoneName': 'ping', 'zoneState': {'available': True}},
+                {'zoneName': 'pong', 'zoneState': {'available': False}},
+            ],
+        }
+
+        serializer = cinder.api.contrib.availability_zones.ListTemplate()
+        text = serializer.serialize(fixture)
+        tree = etree.fromstring(text)
+
+        self.assertEqual('availabilityZones', tree.tag)
+        self.assertEqual(2, len(tree))
+
+        self.assertEqual('availabilityZone', tree[0].tag)
+
+        self.assertEqual('ping', tree[0].get('name'))
+        self.assertEqual('zoneState', tree[0][0].tag)
+        self.assertEqual('True', tree[0][0].get('available'))
+
+        self.assertEqual('pong', tree[1].get('name'))
+        self.assertEqual('zoneState', tree[1][0].tag)
+        self.assertEqual('False', tree[1][0].get('available'))
index 6bd658589f3995f237ca41b59ddeefb4b382ef46..f406d8dfc29eda73a072831ffdb2043a8f44d6fc 100644 (file)
@@ -132,4 +132,4 @@ def stub_snapshot_update(self, context, *args, **param):
 
 
 def stub_service_get_all_by_topic(context, topic):
-    return [{'availability_zone': "zone1:host1"}]
+    return [{'availability_zone': "zone1:host1", "disabled": 0}]
index a27202fe35134025da99ac76fae2f14ee63d7ea2..712558364b5698841af7ac6ad93ed8b4eb2f3658 100644 (file)
@@ -139,4 +139,4 @@ def stub_snapshot_update(self, context, *args, **param):
 
 
 def stub_service_get_all_by_topic(context, topic):
-    return [{'availability_zone': "zone1:host1"}]
+    return [{'availability_zone': "zone1:host1", "disabled": 0}]
index a2cdfe7bf2cdab83842d1d8e3d0c10e53889071f..2494e53818c0489cdd5e23c6c33f4224dc9c210e 100644 (file)
@@ -1273,6 +1273,31 @@ class VolumeTestCase(test.TestCase):
         self.volume.delete_volume(self.context, volume_dst['id'])
         self.volume.delete_volume(self.context, volume_src['id'])
 
+    def test_list_availability_zones_enabled_service(self):
+        services = [
+            {'availability_zone': 'ping', 'disabled': 0},
+            {'availability_zone': 'ping', 'disabled': 1},
+            {'availability_zone': 'pong', 'disabled': 0},
+            {'availability_zone': 'pung', 'disabled': 1},
+        ]
+
+        def stub_service_get_all_by_topic(*args, **kwargs):
+            return services
+
+        self.stubs.Set(db, 'service_get_all_by_topic',
+                       stub_service_get_all_by_topic)
+
+        volume_api = cinder.volume.api.API()
+        azs = volume_api.list_availability_zones()
+
+        expected = (
+            {'name': 'pung', 'available': False},
+            {'name': 'pong', 'available': True},
+            {'name': 'ping', 'available': True},
+        )
+
+        self.assertEqual(expected, azs)
+
 
 class DriverTestCase(test.TestCase):
     """Base Test class for Drivers."""
index 0ff722f306f7d2fc97453af26be942de26f6eec2..71d0cdc9b00b113f36665f6838c040e11745cf54 100644 (file)
@@ -86,7 +86,7 @@ class API(base.Base):
                               glance.get_default_image_service())
         self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI()
         self.volume_rpcapi = volume_rpcapi.VolumeAPI()
-        self.availability_zones = set()
+        self.availability_zone_names = ()
         super(API, self).__init__(db_driver)
 
     def create(self, context, size, name, description, snapshot=None,
@@ -298,24 +298,39 @@ class API(base.Base):
                 filter_properties=filter_properties)
 
     def _check_availabilty_zone(self, availability_zone):
-        if availability_zone in self.availability_zones:
+        #NOTE(bcwaldon): This approach to caching fails to handle the case
+        # that an availability zone is disabled/removed.
+        if availability_zone in self.availability_zone_names:
             return
 
-        ctxt = context.get_admin_context()
-        topic = CONF.volume_topic
-        volume_services = self.db.service_get_all_by_topic(ctxt, topic)
-
-        # NOTE(haomai): In case of volume services isn't init or
-        # availability_zones is updated in the backend
-        self.availability_zones = set()
-        for service in volume_services:
-            self.availability_zones.add(service['availability_zone'])
+        azs = self.list_availability_zones()
+        self.availability_zone_names = [az['name'] for az in azs]
 
-        if availability_zone not in self.availability_zones:
+        if availability_zone not in self.availability_zone_names:
             msg = _("Availability zone is invalid")
             LOG.warn(msg)
             raise exception.InvalidInput(reason=msg)
 
+    def list_availability_zones(self):
+        """Describe the known availability zones
+
+        :retval list of dicts, each with a 'name' and 'available' key
+        """
+        topic = CONF.volume_topic
+        ctxt = context.get_admin_context()
+        services = self.db.service_get_all_by_topic(ctxt, topic)
+        az_data = [(s['availability_zone'], s['disabled']) for s in services]
+
+        disabled_map = {}
+        for (az_name, disabled) in az_data:
+            tracked_disabled = disabled_map.get(az_name, True)
+            disabled_map[az_name] = tracked_disabled and disabled
+
+        azs = [{'name': name, 'available': not disabled}
+               for (name, disabled) in disabled_map.items()]
+
+        return tuple(azs)
+
     @wrap_check_policy
     def delete(self, context, volume, force=False):
         if context.is_admin and context.project_id != volume['project_id']: