--- /dev/null
+# 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]
--- /dev/null
+# 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]}
--- /dev/null
+# 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'))
def stub_service_get_all_by_topic(context, topic):
- return [{'availability_zone': "zone1:host1"}]
+ return [{'availability_zone': "zone1:host1", "disabled": 0}]
def stub_service_get_all_by_topic(context, topic):
- return [{'availability_zone': "zone1:host1"}]
+ return [{'availability_zone': "zone1:host1", "disabled": 0}]
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."""
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,
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']: