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
This commit is contained in:
Brian Waldon
2013-06-26 09:43:48 -07:00
parent 6f3b40c59d
commit 993afc46c0
7 changed files with 243 additions and 14 deletions

View File

@@ -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]

View File

@@ -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]}

View File

@@ -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'))

View 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}]

View 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}]

View 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."""

View 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)
azs = self.list_availability_zones()
self.availability_zone_names = [az['name'] for az in azs]
# 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'])
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']: