From 993afc46c0fb674a89a620671a0d82f0c2daf0aa Mon Sep 17 00:00:00 2001 From: Brian Waldon Date: Wed, 26 Jun 2013 09:43:48 -0700 Subject: [PATCH] Add os-availability-zone extension * 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 | 70 +++++++++++++++ cinder/api/views/availability_zones.py | 29 ++++++ .../api/contrib/test_availability_zones.py | 90 +++++++++++++++++++ cinder/tests/api/v1/stubs.py | 2 +- cinder/tests/api/v2/stubs.py | 2 +- cinder/tests/test_volume.py | 25 ++++++ cinder/volume/api.py | 39 +++++--- 7 files changed, 243 insertions(+), 14 deletions(-) create mode 100644 cinder/api/contrib/availability_zones.py create mode 100644 cinder/api/views/availability_zones.py create mode 100644 cinder/tests/api/contrib/test_availability_zones.py diff --git a/cinder/api/contrib/availability_zones.py b/cinder/api/contrib/availability_zones.py new file mode 100644 index 000000000..fd2344c49 --- /dev/null +++ b/cinder/api/contrib/availability_zones.py @@ -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 index 000000000..5031cc27f --- /dev/null +++ b/cinder/api/views/availability_zones.py @@ -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 index 000000000..1c2bed8c3 --- /dev/null +++ b/cinder/tests/api/contrib/test_availability_zones.py @@ -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')) diff --git a/cinder/tests/api/v1/stubs.py b/cinder/tests/api/v1/stubs.py index 6bd658589..f406d8dfc 100644 --- a/cinder/tests/api/v1/stubs.py +++ b/cinder/tests/api/v1/stubs.py @@ -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}] diff --git a/cinder/tests/api/v2/stubs.py b/cinder/tests/api/v2/stubs.py index a27202fe3..712558364 100644 --- a/cinder/tests/api/v2/stubs.py +++ b/cinder/tests/api/v2/stubs.py @@ -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}] diff --git a/cinder/tests/test_volume.py b/cinder/tests/test_volume.py index a2cdfe7bf..2494e5381 100644 --- a/cinder/tests/test_volume.py +++ b/cinder/tests/test_volume.py @@ -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.""" diff --git a/cinder/volume/api.py b/cinder/volume/api.py index 0ff722f30..71d0cdc9b 100644 --- a/cinder/volume/api.py +++ b/cinder/volume/api.py @@ -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']: -- 2.45.2