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:
70
cinder/api/contrib/availability_zones.py
Normal file
70
cinder/api/contrib/availability_zones.py
Normal 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]
|
||||
29
cinder/api/views/availability_zones.py
Normal file
29
cinder/api/views/availability_zones.py
Normal 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]}
|
||||
90
cinder/tests/api/contrib/test_availability_zones.py
Normal file
90
cinder/tests/api/contrib/test_availability_zones.py
Normal 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'))
|
||||
@@ -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}]
|
||||
|
||||
@@ -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}]
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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']:
|
||||
|
||||
Reference in New Issue
Block a user