Port types and extra specs to volume api
* Fixes bug 979089 * Adds pollicy for new extensions * Fixes __init__ in testing directories * Cleans up existing volume types tests * Adds tests for type management * Adds tests for extra specs management * Removed unused Quota handling * Fixed typo in db volume_type_get Change-Id: Ic80190ecf1d6d6ad0229e5af642a50c7c53bbbf9
This commit is contained in:
parent
042a4d0d96
commit
15c0847341
@ -65,6 +65,10 @@
|
||||
"volume:get_all_snapshots": [],
|
||||
|
||||
|
||||
"volume_extension:types_manage": [["rule:admin_api"]],
|
||||
"volume_extension:types_extra_specs": [["rule:admin_api"]],
|
||||
|
||||
|
||||
"network:get_all_networks": [],
|
||||
"network:get_network": [],
|
||||
"network:delete_network": [],
|
||||
|
@ -85,8 +85,6 @@ class VolumeTypesController(object):
|
||||
try:
|
||||
volume_types.create(context, name, specs)
|
||||
vol_type = volume_types.get_volume_type_by_name(context, name)
|
||||
except exception.QuotaError as error:
|
||||
self._handle_quota_error(error)
|
||||
except exception.NotFound:
|
||||
raise exc.HTTPNotFound()
|
||||
|
||||
@ -171,12 +169,9 @@ class VolumeTypeExtraSpecsController(object):
|
||||
authorize(context)
|
||||
self._check_body(body)
|
||||
specs = body.get('extra_specs')
|
||||
try:
|
||||
db.volume_type_extra_specs_update_or_create(context,
|
||||
vol_type_id,
|
||||
specs)
|
||||
except exception.QuotaError as error:
|
||||
self._handle_quota_error(error)
|
||||
return body
|
||||
|
||||
@wsgi.serializers(xml=VolumeTypeExtraSpecTemplate)
|
||||
@ -190,13 +185,9 @@ class VolumeTypeExtraSpecsController(object):
|
||||
if len(body) > 1:
|
||||
expl = _('Request body contains too many items')
|
||||
raise exc.HTTPBadRequest(explanation=expl)
|
||||
try:
|
||||
db.volume_type_extra_specs_update_or_create(context,
|
||||
vol_type_id,
|
||||
body)
|
||||
except exception.QuotaError as error:
|
||||
self._handle_quota_error(error)
|
||||
|
||||
return body
|
||||
|
||||
@wsgi.serializers(xml=VolumeTypeExtraSpecTemplate)
|
||||
@ -216,12 +207,6 @@ class VolumeTypeExtraSpecsController(object):
|
||||
authorize(context)
|
||||
db.volume_type_extra_specs_delete(context, vol_type_id, id)
|
||||
|
||||
def _handle_quota_error(self, error):
|
||||
"""Reraise quota errors as api-specific http exceptions."""
|
||||
if error.code == "MetadataLimitExceeded":
|
||||
raise exc.HTTPBadRequest(explanation=error.message)
|
||||
raise error
|
||||
|
||||
|
||||
class Volumetypes(extensions.ExtensionDescriptor):
|
||||
"""Volume types support"""
|
||||
|
152
nova/api/openstack/volume/contrib/types_extra_specs.py
Normal file
152
nova/api/openstack/volume/contrib/types_extra_specs.py
Normal file
@ -0,0 +1,152 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Zadara Storage Inc.
|
||||
# Copyright (c) 2011 OpenStack LLC.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""The volume types extra specs extension"""
|
||||
|
||||
import webob
|
||||
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import wsgi
|
||||
from nova.api.openstack import xmlutil
|
||||
from nova import db
|
||||
from nova import exception
|
||||
from nova.volume import volume_types
|
||||
|
||||
|
||||
authorize = extensions.extension_authorizer('volume', 'types_extra_specs')
|
||||
|
||||
|
||||
class VolumeTypeExtraSpecsTemplate(xmlutil.TemplateBuilder):
|
||||
def construct(self):
|
||||
root = xmlutil.make_flat_dict('extra_specs', selector='extra_specs')
|
||||
return xmlutil.MasterTemplate(root, 1)
|
||||
|
||||
|
||||
class VolumeTypeExtraSpecTemplate(xmlutil.TemplateBuilder):
|
||||
def construct(self):
|
||||
tagname = xmlutil.Selector('key')
|
||||
|
||||
def extraspec_sel(obj, do_raise=False):
|
||||
# Have to extract the key and value for later use...
|
||||
key, value = obj.items()[0]
|
||||
return dict(key=key, value=value)
|
||||
|
||||
root = xmlutil.TemplateElement(tagname, selector=extraspec_sel)
|
||||
root.text = 'value'
|
||||
return xmlutil.MasterTemplate(root, 1)
|
||||
|
||||
|
||||
class VolumeTypeExtraSpecsController(object):
|
||||
""" The volume type extra specs API controller for the OpenStack API """
|
||||
|
||||
def _get_extra_specs(self, context, type_id):
|
||||
extra_specs = db.volume_type_extra_specs_get(context, type_id)
|
||||
specs_dict = {}
|
||||
for key, value in extra_specs.iteritems():
|
||||
specs_dict[key] = value
|
||||
return dict(extra_specs=specs_dict)
|
||||
|
||||
def _check_body(self, body):
|
||||
if not body:
|
||||
expl = _('No Request Body')
|
||||
raise webob.exc.HTTPBadRequest(explanation=expl)
|
||||
|
||||
def _check_type(self, context, type_id):
|
||||
try:
|
||||
volume_types.get_volume_type(context, type_id)
|
||||
except exception.NotFound as ex:
|
||||
raise webob.exc.HTTPNotFound(explanation=unicode(ex))
|
||||
|
||||
@wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate)
|
||||
def index(self, req, type_id):
|
||||
""" Returns the list of extra specs for a given volume type """
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
self._check_type(context, type_id)
|
||||
return self._get_extra_specs(context, type_id)
|
||||
|
||||
@wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate)
|
||||
def create(self, req, type_id, body=None):
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
self._check_type(context, type_id)
|
||||
self._check_body(body)
|
||||
specs = body.get('extra_specs')
|
||||
if not isinstance(specs, dict):
|
||||
expl = _('Malformed extra specs')
|
||||
raise webob.exc.HTTPBadRequest(explanation=expl)
|
||||
db.volume_type_extra_specs_update_or_create(context,
|
||||
type_id,
|
||||
specs)
|
||||
return body
|
||||
|
||||
@wsgi.serializers(xml=VolumeTypeExtraSpecTemplate)
|
||||
def update(self, req, type_id, id, body=None):
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
self._check_type(context, type_id)
|
||||
self._check_body(body)
|
||||
if not id in body:
|
||||
expl = _('Request body and URI mismatch')
|
||||
raise webob.exc.HTTPBadRequest(explanation=expl)
|
||||
if len(body) > 1:
|
||||
expl = _('Request body contains too many items')
|
||||
raise webob.exc.HTTPBadRequest(explanation=expl)
|
||||
db.volume_type_extra_specs_update_or_create(context,
|
||||
type_id,
|
||||
body)
|
||||
return body
|
||||
|
||||
@wsgi.serializers(xml=VolumeTypeExtraSpecTemplate)
|
||||
def show(self, req, type_id, id):
|
||||
"""Return a single extra spec item."""
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
self._check_type(context, type_id)
|
||||
specs = self._get_extra_specs(context, type_id)
|
||||
if id in specs['extra_specs']:
|
||||
return {id: specs['extra_specs'][id]}
|
||||
else:
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
def delete(self, req, type_id, id):
|
||||
""" Deletes an existing extra spec """
|
||||
context = req.environ['nova.context']
|
||||
self._check_type(context, type_id)
|
||||
authorize(context)
|
||||
db.volume_type_extra_specs_delete(context, type_id, id)
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
|
||||
class Types_extra_specs(extensions.ExtensionDescriptor):
|
||||
"""Types extra specs support"""
|
||||
|
||||
name = "TypesExtraSpecs"
|
||||
alias = "os-types-extra-specs"
|
||||
namespace = "http://docs.openstack.org/volume/ext/types-extra-specs/api/v1"
|
||||
updated = "2011-08-24T00:00:00+00:00"
|
||||
|
||||
def get_resources(self):
|
||||
resources = []
|
||||
res = extensions.ResourceExtension('extra_specs',
|
||||
VolumeTypeExtraSpecsController(),
|
||||
parent=dict(
|
||||
member_name='type',
|
||||
collection_name='types'))
|
||||
resources.append(res)
|
||||
|
||||
return resources
|
91
nova/api/openstack/volume/contrib/types_manage.py
Normal file
91
nova/api/openstack/volume/contrib/types_manage.py
Normal file
@ -0,0 +1,91 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Zadara Storage Inc.
|
||||
# Copyright (c) 2011 OpenStack LLC.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""The volume types manage extension."""
|
||||
|
||||
import webob
|
||||
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack.volume import types
|
||||
from nova.api.openstack import wsgi
|
||||
from nova import exception
|
||||
from nova.volume import volume_types
|
||||
|
||||
|
||||
authorize = extensions.extension_authorizer('volume', 'types_manage')
|
||||
|
||||
|
||||
class VolumeTypesManageController(wsgi.Controller):
|
||||
""" The volume types API controller for the OpenStack API """
|
||||
|
||||
@wsgi.action("create")
|
||||
@wsgi.serializers(xml=types.VolumeTypeTemplate)
|
||||
def _create(self, req, body):
|
||||
"""Creates a new volume type."""
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
|
||||
if not body or body == "":
|
||||
raise webob.exc.HTTPUnprocessableEntity()
|
||||
|
||||
vol_type = body.get('volume_type', None)
|
||||
if vol_type is None or vol_type == "":
|
||||
raise webob.exc.HTTPUnprocessableEntity()
|
||||
|
||||
name = vol_type.get('name', None)
|
||||
specs = vol_type.get('extra_specs', {})
|
||||
|
||||
if name is None or name == "":
|
||||
raise webob.exc.HTTPUnprocessableEntity()
|
||||
|
||||
try:
|
||||
volume_types.create(context, name, specs)
|
||||
vol_type = volume_types.get_volume_type_by_name(context, name)
|
||||
except exception.VolumeTypeExists as err:
|
||||
raise webob.exc.HTTPConflict(explanation=str(err))
|
||||
except exception.NotFound:
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
return {'volume_type': vol_type}
|
||||
|
||||
@wsgi.action("delete")
|
||||
def _delete(self, req, id):
|
||||
""" Deletes an existing volume type """
|
||||
context = req.environ['nova.context']
|
||||
authorize(context)
|
||||
|
||||
try:
|
||||
vol_type = volume_types.get_volume_type(context, id)
|
||||
volume_types.destroy(context, vol_type['name'])
|
||||
except exception.NotFound:
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
|
||||
class Types_manage(extensions.ExtensionDescriptor):
|
||||
"""Types manage support"""
|
||||
|
||||
name = "TypesManage"
|
||||
alias = "os-types-manage"
|
||||
namespace = "http://docs.openstack.org/volume/ext/types-manage/api/v1"
|
||||
updated = "2011-08-24T00:00:00+00:00"
|
||||
|
||||
def get_controller_extensions(self):
|
||||
controller = VolumeTypesManageController()
|
||||
extension = extensions.ControllerExtension(self, 'types', controller)
|
||||
return [extension]
|
@ -3866,7 +3866,7 @@ def volume_type_get(context, id, session=None):
|
||||
first()
|
||||
|
||||
if not result:
|
||||
raise exception.VolumeTypeNotFound(volume_type=id)
|
||||
raise exception.VolumeTypeNotFound(volume_type_id=id)
|
||||
|
||||
return _dict_with_extra_specs(result)
|
||||
|
||||
|
@ -0,0 +1,19 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# 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.
|
||||
|
||||
# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work
|
||||
from nova.tests import *
|
@ -1,6 +1,6 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 OpenStack LLC.
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -14,3 +14,6 @@
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work
|
||||
from nova.tests import *
|
||||
|
@ -1,6 +1,7 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# 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
|
||||
@ -13,3 +14,6 @@
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work
|
||||
from nova.tests import *
|
||||
|
@ -1,6 +1,6 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 OpenStack LLC.
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -14,3 +14,6 @@
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work
|
||||
from nova.tests import *
|
||||
|
19
nova/tests/api/openstack/volume/contrib/__init__.py
Normal file
19
nova/tests/api/openstack/volume/contrib/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# 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.
|
||||
|
||||
# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work
|
||||
from nova.tests import *
|
@ -0,0 +1,202 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Zadara Storage Inc.
|
||||
# Copyright (c) 2011 OpenStack LLC.
|
||||
# Copyright 2011 University of Southern California
|
||||
# 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 lxml import etree
|
||||
import webob
|
||||
|
||||
from nova.api.openstack.volume.contrib import types_extra_specs
|
||||
from nova import test
|
||||
from nova.tests.api.openstack import fakes
|
||||
import nova.wsgi
|
||||
|
||||
|
||||
def return_create_volume_type_extra_specs(context, volume_type_id,
|
||||
extra_specs):
|
||||
return stub_volume_type_extra_specs()
|
||||
|
||||
|
||||
def return_volume_type_extra_specs(context, volume_type_id):
|
||||
return stub_volume_type_extra_specs()
|
||||
|
||||
|
||||
def return_empty_volume_type_extra_specs(context, volume_type_id):
|
||||
return {}
|
||||
|
||||
|
||||
def delete_volume_type_extra_specs(context, volume_type_id, key):
|
||||
pass
|
||||
|
||||
|
||||
def stub_volume_type_extra_specs():
|
||||
specs = {
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
"key4": "value4",
|
||||
"key5": "value5"}
|
||||
return specs
|
||||
|
||||
|
||||
def volume_type_get(context, volume_type_id):
|
||||
pass
|
||||
|
||||
|
||||
class VolumeTypesExtraSpecsTest(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(VolumeTypesExtraSpecsTest, self).setUp()
|
||||
self.stubs.Set(nova.db, 'volume_type_get', volume_type_get)
|
||||
self.api_path = '/v1/fake/os-volume-types/1/extra_specs'
|
||||
self.controller = types_extra_specs.VolumeTypeExtraSpecsController()
|
||||
|
||||
def test_index(self):
|
||||
self.stubs.Set(nova.db, 'volume_type_extra_specs_get',
|
||||
return_volume_type_extra_specs)
|
||||
|
||||
req = fakes.HTTPRequest.blank(self.api_path)
|
||||
res_dict = self.controller.index(req, 1)
|
||||
|
||||
self.assertEqual('value1', res_dict['extra_specs']['key1'])
|
||||
|
||||
def test_index_no_data(self):
|
||||
self.stubs.Set(nova.db, 'volume_type_extra_specs_get',
|
||||
return_empty_volume_type_extra_specs)
|
||||
|
||||
req = fakes.HTTPRequest.blank(self.api_path)
|
||||
res_dict = self.controller.index(req, 1)
|
||||
|
||||
self.assertEqual(0, len(res_dict['extra_specs']))
|
||||
|
||||
def test_show(self):
|
||||
self.stubs.Set(nova.db, 'volume_type_extra_specs_get',
|
||||
return_volume_type_extra_specs)
|
||||
|
||||
req = fakes.HTTPRequest.blank(self.api_path + '/key5')
|
||||
res_dict = self.controller.show(req, 1, 'key5')
|
||||
|
||||
self.assertEqual('value5', res_dict['key5'])
|
||||
|
||||
def test_show_spec_not_found(self):
|
||||
self.stubs.Set(nova.db, 'volume_type_extra_specs_get',
|
||||
return_empty_volume_type_extra_specs)
|
||||
|
||||
req = fakes.HTTPRequest.blank(self.api_path + '/key6')
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
|
||||
req, 1, 'key6')
|
||||
|
||||
def test_delete(self):
|
||||
self.stubs.Set(nova.db, 'volume_type_extra_specs_delete',
|
||||
delete_volume_type_extra_specs)
|
||||
|
||||
req = fakes.HTTPRequest.blank(self.api_path + '/key5')
|
||||
self.controller.delete(req, 1, 'key5')
|
||||
|
||||
def test_create(self):
|
||||
self.stubs.Set(nova.db,
|
||||
'volume_type_extra_specs_update_or_create',
|
||||
return_create_volume_type_extra_specs)
|
||||
body = {"extra_specs": {"key1": "value1"}}
|
||||
|
||||
req = fakes.HTTPRequest.blank(self.api_path)
|
||||
res_dict = self.controller.create(req, 1, body)
|
||||
|
||||
self.assertEqual('value1', res_dict['extra_specs']['key1'])
|
||||
|
||||
def test_create_empty_body(self):
|
||||
self.stubs.Set(nova.db,
|
||||
'volume_type_extra_specs_update_or_create',
|
||||
return_create_volume_type_extra_specs)
|
||||
|
||||
req = fakes.HTTPRequest.blank(self.api_path)
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
|
||||
req, 1, '')
|
||||
|
||||
def test_update_item(self):
|
||||
self.stubs.Set(nova.db,
|
||||
'volume_type_extra_specs_update_or_create',
|
||||
return_create_volume_type_extra_specs)
|
||||
body = {"key1": "value1"}
|
||||
|
||||
req = fakes.HTTPRequest.blank(self.api_path + '/key1')
|
||||
res_dict = self.controller.update(req, 1, 'key1', body)
|
||||
|
||||
self.assertEqual('value1', res_dict['key1'])
|
||||
|
||||
def test_update_item_empty_body(self):
|
||||
self.stubs.Set(nova.db,
|
||||
'volume_type_extra_specs_update_or_create',
|
||||
return_create_volume_type_extra_specs)
|
||||
|
||||
req = fakes.HTTPRequest.blank(self.api_path + '/key1')
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
|
||||
req, 1, 'key1', '')
|
||||
|
||||
def test_update_item_too_many_keys(self):
|
||||
self.stubs.Set(nova.db,
|
||||
'volume_type_extra_specs_update_or_create',
|
||||
return_create_volume_type_extra_specs)
|
||||
body = {"key1": "value1", "key2": "value2"}
|
||||
|
||||
req = fakes.HTTPRequest.blank(self.api_path + '/key1')
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
|
||||
req, 1, 'key1', body)
|
||||
|
||||
def test_update_item_body_uri_mismatch(self):
|
||||
self.stubs.Set(nova.db,
|
||||
'volume_type_extra_specs_update_or_create',
|
||||
return_create_volume_type_extra_specs)
|
||||
body = {"key1": "value1"}
|
||||
|
||||
req = fakes.HTTPRequest.blank(self.api_path + '/bad')
|
||||
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
|
||||
req, 1, 'bad', body)
|
||||
|
||||
|
||||
class VolumeTypeExtraSpecsSerializerTest(test.TestCase):
|
||||
def test_index_create_serializer(self):
|
||||
serializer = types_extra_specs.VolumeTypeExtraSpecsTemplate()
|
||||
|
||||
# Just getting some input data
|
||||
extra_specs = stub_volume_type_extra_specs()
|
||||
text = serializer.serialize(dict(extra_specs=extra_specs))
|
||||
|
||||
print text
|
||||
tree = etree.fromstring(text)
|
||||
|
||||
self.assertEqual('extra_specs', tree.tag)
|
||||
self.assertEqual(len(extra_specs), len(tree))
|
||||
seen = set(extra_specs.keys())
|
||||
for child in tree:
|
||||
self.assertTrue(child.tag in seen)
|
||||
self.assertEqual(extra_specs[child.tag], child.text)
|
||||
seen.remove(child.tag)
|
||||
self.assertEqual(len(seen), 0)
|
||||
|
||||
def test_update_show_serializer(self):
|
||||
serializer = types_extra_specs.VolumeTypeExtraSpecTemplate()
|
||||
|
||||
exemplar = dict(key1='value1')
|
||||
text = serializer.serialize(exemplar)
|
||||
|
||||
print text
|
||||
tree = etree.fromstring(text)
|
||||
|
||||
self.assertEqual('key1', tree.tag)
|
||||
self.assertEqual('value1', tree.text)
|
||||
self.assertEqual(0, len(tree))
|
103
nova/tests/api/openstack/volume/contrib/test_types_manage.py
Normal file
103
nova/tests/api/openstack/volume/contrib/test_types_manage.py
Normal file
@ -0,0 +1,103 @@
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# 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 webob
|
||||
|
||||
from nova.api.openstack.volume.contrib import types_manage
|
||||
from nova import exception
|
||||
from nova import test
|
||||
from nova.volume import volume_types
|
||||
from nova.tests.api.openstack import fakes
|
||||
|
||||
|
||||
def stub_volume_type(id):
|
||||
specs = {
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
"key4": "value4",
|
||||
"key5": "value5"}
|
||||
return dict(id=id, name='vol_type_%s' % str(id), extra_specs=specs)
|
||||
|
||||
|
||||
def return_volume_types_get_volume_type(context, id):
|
||||
if id == "777":
|
||||
raise exception.VolumeTypeNotFound(volume_type_id=id)
|
||||
return stub_volume_type(int(id))
|
||||
|
||||
|
||||
def return_volume_types_destroy(context, name):
|
||||
if name == "777":
|
||||
raise exception.VolumeTypeNotFoundByName(volume_type_name=name)
|
||||
pass
|
||||
|
||||
|
||||
def return_volume_types_create(context, name, specs):
|
||||
pass
|
||||
|
||||
|
||||
def return_volume_types_get_by_name(context, name):
|
||||
if name == "777":
|
||||
raise exception.VolumeTypeNotFoundByName(volume_type_name=name)
|
||||
return stub_volume_type(int(name.split("_")[2]))
|
||||
|
||||
|
||||
class VolumeTypesManageApiTest(test.TestCase):
|
||||
def setUp(self):
|
||||
super(VolumeTypesManageApiTest, self).setUp()
|
||||
self.controller = types_manage.VolumeTypesManageController()
|
||||
|
||||
def test_volume_types_delete(self):
|
||||
self.stubs.Set(volume_types, 'get_volume_type',
|
||||
return_volume_types_get_volume_type)
|
||||
self.stubs.Set(volume_types, 'destroy',
|
||||
return_volume_types_destroy)
|
||||
|
||||
req = fakes.HTTPRequest.blank('/v1/fake/types/1')
|
||||
self.controller._delete(req, 1)
|
||||
|
||||
def test_volume_types_delete_not_found(self):
|
||||
self.stubs.Set(volume_types, 'get_volume_type',
|
||||
return_volume_types_get_volume_type)
|
||||
self.stubs.Set(volume_types, 'destroy',
|
||||
return_volume_types_destroy)
|
||||
|
||||
req = fakes.HTTPRequest.blank('/v1/fake/types/777')
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller._delete,
|
||||
req, '777')
|
||||
|
||||
def test_create(self):
|
||||
self.stubs.Set(volume_types, 'create',
|
||||
return_volume_types_create)
|
||||
self.stubs.Set(volume_types, 'get_volume_type_by_name',
|
||||
return_volume_types_get_by_name)
|
||||
|
||||
body = {"volume_type": {"name": "vol_type_1",
|
||||
"extra_specs": {"key1": "value1"}}}
|
||||
req = fakes.HTTPRequest.blank('/v1/fake/types')
|
||||
res_dict = self.controller._create(req, body)
|
||||
|
||||
self.assertEqual(1, len(res_dict))
|
||||
self.assertEqual('vol_type_1', res_dict['volume_type']['name'])
|
||||
|
||||
def test_create_empty_body(self):
|
||||
self.stubs.Set(volume_types, 'create',
|
||||
return_volume_types_create)
|
||||
self.stubs.Set(volume_types, 'get_volume_type_by_name',
|
||||
return_volume_types_get_by_name)
|
||||
|
||||
req = fakes.HTTPRequest.blank('/v1/fake/types')
|
||||
self.assertRaises(webob.exc.HTTPUnprocessableEntity,
|
||||
self.controller._create, req, '')
|
15
nova/tests/api/openstack/volume/extensions/__init__.py
Normal file
15
nova/tests/api/openstack/volume/extensions/__init__.py
Normal file
@ -0,0 +1,15 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC
|
||||
#
|
||||
# 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.
|
94
nova/tests/api/openstack/volume/extensions/foxinsocks.py
Normal file
94
nova/tests/api/openstack/volume/extensions/foxinsocks.py
Normal file
@ -0,0 +1,94 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# 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 webob.exc
|
||||
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import wsgi
|
||||
|
||||
|
||||
class FoxInSocksController(object):
|
||||
|
||||
def index(self, req):
|
||||
return "Try to say this Mr. Knox, sir..."
|
||||
|
||||
|
||||
class FoxInSocksServerControllerExtension(wsgi.Controller):
|
||||
@wsgi.action('add_tweedle')
|
||||
def _add_tweedle(self, req, id, body):
|
||||
|
||||
return "Tweedle Beetle Added."
|
||||
|
||||
@wsgi.action('delete_tweedle')
|
||||
def _delete_tweedle(self, req, id, body):
|
||||
|
||||
return "Tweedle Beetle Deleted."
|
||||
|
||||
@wsgi.action('fail')
|
||||
def _fail(self, req, id, body):
|
||||
|
||||
raise webob.exc.HTTPBadRequest(explanation='Tweedle fail')
|
||||
|
||||
|
||||
class FoxInSocksFlavorGooseControllerExtension(wsgi.Controller):
|
||||
@wsgi.extends
|
||||
def show(self, req, resp_obj, id):
|
||||
#NOTE: This only handles JSON responses.
|
||||
# You can use content type header to test for XML.
|
||||
resp_obj.obj['flavor']['googoose'] = req.GET.get('chewing')
|
||||
|
||||
|
||||
class FoxInSocksFlavorBandsControllerExtension(wsgi.Controller):
|
||||
@wsgi.extends
|
||||
def show(self, req, resp_obj, id):
|
||||
#NOTE: This only handles JSON responses.
|
||||
# You can use content type header to test for XML.
|
||||
resp_obj.obj['big_bands'] = 'Pig Bands!'
|
||||
|
||||
|
||||
class Foxinsocks(extensions.ExtensionDescriptor):
|
||||
"""The Fox In Socks Extension"""
|
||||
|
||||
name = "Fox In Socks"
|
||||
alias = "FOXNSOX"
|
||||
namespace = "http://www.fox.in.socks/api/ext/pie/v1.0"
|
||||
updated = "2011-01-22T13:25:27-06:00"
|
||||
|
||||
def __init__(self, ext_mgr):
|
||||
ext_mgr.register(self)
|
||||
|
||||
def get_resources(self):
|
||||
resources = []
|
||||
resource = extensions.ResourceExtension('foxnsocks',
|
||||
FoxInSocksController())
|
||||
resources.append(resource)
|
||||
return resources
|
||||
|
||||
def get_controller_extensions(self):
|
||||
extension_list = []
|
||||
|
||||
extension_set = [
|
||||
(FoxInSocksServerControllerExtension, 'servers'),
|
||||
(FoxInSocksFlavorGooseControllerExtension, 'flavors'),
|
||||
(FoxInSocksFlavorBandsControllerExtension, 'flavors'),
|
||||
]
|
||||
for klass, collection in extension_set:
|
||||
controller = klass()
|
||||
ext = extensions.ControllerExtension(self, collection, controller)
|
||||
extension_list.append(ext)
|
||||
|
||||
return extension_list
|
156
nova/tests/api/openstack/volume/test_extensions.py
Normal file
156
nova/tests/api/openstack/volume/test_extensions.py
Normal file
@ -0,0 +1,156 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 X.commerce, a business unit of eBay Inc.
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# 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 json
|
||||
|
||||
import webob
|
||||
from lxml import etree
|
||||
import iso8601
|
||||
|
||||
from nova.api.openstack import volume
|
||||
from nova.api.openstack import xmlutil
|
||||
from nova import flags
|
||||
from nova import test
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
NS = "{http://docs.openstack.org/common/api/v1.0}"
|
||||
|
||||
|
||||
class ExtensionTestCase(test.TestCase):
|
||||
def setUp(self):
|
||||
super(ExtensionTestCase, self).setUp()
|
||||
ext_list = FLAGS.osapi_volume_extension[:]
|
||||
fox = ('nova.tests.api.openstack.volume.extensions.'
|
||||
'foxinsocks.Foxinsocks')
|
||||
if fox not in ext_list:
|
||||
ext_list.append(fox)
|
||||
self.flags(osapi_volume_extension=ext_list)
|
||||
|
||||
|
||||
class ExtensionControllerTest(ExtensionTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ExtensionControllerTest, self).setUp()
|
||||
self.ext_list = [
|
||||
"TypesManage",
|
||||
"TypesExtraSpecs",
|
||||
]
|
||||
self.ext_list.sort()
|
||||
|
||||
def test_list_extensions_json(self):
|
||||
app = volume.APIRouter()
|
||||
request = webob.Request.blank("/fake/extensions")
|
||||
response = request.get_response(app)
|
||||
self.assertEqual(200, response.status_int)
|
||||
|
||||
# Make sure we have all the extensions, extra extensions being OK.
|
||||
data = json.loads(response.body)
|
||||
names = [str(x['name']) for x in data['extensions']
|
||||
if str(x['name']) in self.ext_list]
|
||||
names.sort()
|
||||
self.assertEqual(names, self.ext_list)
|
||||
|
||||
# Ensure all the timestamps are valid according to iso8601
|
||||
for ext in data['extensions']:
|
||||
iso8601.parse_date(ext['updated'])
|
||||
|
||||
# Make sure that at least Fox in Sox is correct.
|
||||
(fox_ext, ) = [
|
||||
x for x in data['extensions'] if x['alias'] == 'FOXNSOX']
|
||||
self.assertEqual(fox_ext, {
|
||||
'namespace': 'http://www.fox.in.socks/api/ext/pie/v1.0',
|
||||
'name': 'Fox In Socks',
|
||||
'updated': '2011-01-22T13:25:27-06:00',
|
||||
'description': 'The Fox In Socks Extension',
|
||||
'alias': 'FOXNSOX',
|
||||
'links': []
|
||||
},
|
||||
)
|
||||
|
||||
for ext in data['extensions']:
|
||||
url = '/fake/extensions/%s' % ext['alias']
|
||||
request = webob.Request.blank(url)
|
||||
response = request.get_response(app)
|
||||
output = json.loads(response.body)
|
||||
self.assertEqual(output['extension']['alias'], ext['alias'])
|
||||
|
||||
def test_get_extension_json(self):
|
||||
app = volume.APIRouter()
|
||||
request = webob.Request.blank("/fake/extensions/FOXNSOX")
|
||||
response = request.get_response(app)
|
||||
self.assertEqual(200, response.status_int)
|
||||
|
||||
data = json.loads(response.body)
|
||||
self.assertEqual(data['extension'], {
|
||||
"namespace": "http://www.fox.in.socks/api/ext/pie/v1.0",
|
||||
"name": "Fox In Socks",
|
||||
"updated": "2011-01-22T13:25:27-06:00",
|
||||
"description": "The Fox In Socks Extension",
|
||||
"alias": "FOXNSOX",
|
||||
"links": []})
|
||||
|
||||
def test_get_non_existing_extension_json(self):
|
||||
app = volume.APIRouter()
|
||||
request = webob.Request.blank("/fake/extensions/4")
|
||||
response = request.get_response(app)
|
||||
self.assertEqual(404, response.status_int)
|
||||
|
||||
def test_list_extensions_xml(self):
|
||||
app = volume.APIRouter()
|
||||
request = webob.Request.blank("/fake/extensions")
|
||||
request.accept = "application/xml"
|
||||
response = request.get_response(app)
|
||||
self.assertEqual(200, response.status_int)
|
||||
|
||||
root = etree.XML(response.body)
|
||||
self.assertEqual(root.tag.split('extensions')[0], NS)
|
||||
|
||||
# Make sure we have all the extensions, extras extensions being OK.
|
||||
exts = root.findall('{0}extension'.format(NS))
|
||||
self.assert_(len(exts) >= len(self.ext_list))
|
||||
|
||||
# Make sure that at least Fox in Sox is correct.
|
||||
(fox_ext, ) = [x for x in exts if x.get('alias') == 'FOXNSOX']
|
||||
self.assertEqual(fox_ext.get('name'), 'Fox In Socks')
|
||||
self.assertEqual(fox_ext.get('namespace'),
|
||||
'http://www.fox.in.socks/api/ext/pie/v1.0')
|
||||
self.assertEqual(fox_ext.get('updated'), '2011-01-22T13:25:27-06:00')
|
||||
self.assertEqual(fox_ext.findtext('{0}description'.format(NS)),
|
||||
'The Fox In Socks Extension')
|
||||
|
||||
xmlutil.validate_schema(root, 'extensions')
|
||||
|
||||
def test_get_extension_xml(self):
|
||||
app = volume.APIRouter()
|
||||
request = webob.Request.blank("/fake/extensions/FOXNSOX")
|
||||
request.accept = "application/xml"
|
||||
response = request.get_response(app)
|
||||
self.assertEqual(200, response.status_int)
|
||||
xml = response.body
|
||||
|
||||
root = etree.XML(xml)
|
||||
self.assertEqual(root.tag.split('extension')[0], NS)
|
||||
self.assertEqual(root.get('alias'), 'FOXNSOX')
|
||||
self.assertEqual(root.get('name'), 'Fox In Socks')
|
||||
self.assertEqual(root.get('namespace'),
|
||||
'http://www.fox.in.socks/api/ext/pie/v1.0')
|
||||
self.assertEqual(root.get('updated'), '2011-01-22T13:25:27-06:00')
|
||||
self.assertEqual(root.findtext('{0}description'.format(NS)),
|
||||
'The Fox In Socks Extension')
|
||||
|
||||
xmlutil.validate_schema(root, 'extension')
|
@ -19,15 +19,10 @@ import webob
|
||||
from nova.api.openstack.volume import types
|
||||
from nova import exception
|
||||
from nova import test
|
||||
from nova import log as logging
|
||||
from nova.volume import volume_types
|
||||
from nova.tests.api.openstack import fakes
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
last_param = {}
|
||||
|
||||
|
||||
def stub_volume_type(id):
|
||||
specs = {
|
||||
"key1": "value1",
|
||||
@ -54,16 +49,6 @@ def return_volume_types_get_volume_type(context, id):
|
||||
return stub_volume_type(int(id))
|
||||
|
||||
|
||||
def return_volume_types_destroy(context, name):
|
||||
if name == "777":
|
||||
raise exception.VolumeTypeNotFoundByName(volume_type_name=name)
|
||||
pass
|
||||
|
||||
|
||||
def return_volume_types_create(context, name, specs):
|
||||
pass
|
||||
|
||||
|
||||
def return_volume_types_get_by_name(context, name):
|
||||
if name == "777":
|
||||
raise exception.VolumeTypeNotFoundByName(volume_type_name=name)
|
||||
@ -73,14 +58,13 @@ def return_volume_types_get_by_name(context, name):
|
||||
class VolumeTypesApiTest(test.TestCase):
|
||||
def setUp(self):
|
||||
super(VolumeTypesApiTest, self).setUp()
|
||||
fakes.stub_out_key_pair_funcs(self.stubs)
|
||||
self.controller = types.VolumeTypesController()
|
||||
|
||||
def test_volume_types_index(self):
|
||||
self.stubs.Set(volume_types, 'get_all_types',
|
||||
return_volume_types_get_all_types)
|
||||
|
||||
req = fakes.HTTPRequest.blank('/v2/123/os-volume-types')
|
||||
req = fakes.HTTPRequest.blank('/v1/fake/types')
|
||||
res_dict = self.controller.index(req)
|
||||
|
||||
self.assertEqual(3, len(res_dict['volume_types']))
|
||||
@ -95,7 +79,7 @@ class VolumeTypesApiTest(test.TestCase):
|
||||
self.stubs.Set(volume_types, 'get_all_types',
|
||||
return_empty_volume_types_get_all_types)
|
||||
|
||||
req = fakes.HTTPRequest.blank('/v2/123/os-volume-types')
|
||||
req = fakes.HTTPRequest.blank('/v1/fake/types')
|
||||
res_dict = self.controller.index(req)
|
||||
|
||||
self.assertEqual(0, len(res_dict['volume_types']))
|
||||
@ -104,7 +88,7 @@ class VolumeTypesApiTest(test.TestCase):
|
||||
self.stubs.Set(volume_types, 'get_volume_type',
|
||||
return_volume_types_get_volume_type)
|
||||
|
||||
req = fakes.HTTPRequest.blank('/v2/123/os-volume-types/1')
|
||||
req = fakes.HTTPRequest.blank('/v1/fake/types/1')
|
||||
res_dict = self.controller.show(req, 1)
|
||||
|
||||
self.assertEqual(1, len(res_dict))
|
||||
@ -115,7 +99,7 @@ class VolumeTypesApiTest(test.TestCase):
|
||||
self.stubs.Set(volume_types, 'get_volume_type',
|
||||
return_volume_types_get_volume_type)
|
||||
|
||||
req = fakes.HTTPRequest.blank('/v2/123/os-volume-types/777')
|
||||
req = fakes.HTTPRequest.blank('/v1/fake/types/777')
|
||||
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
|
||||
req, '777')
|
||||
|
||||
@ -142,7 +126,6 @@ class VolumeTypesSerializerTest(test.TestCase):
|
||||
vtypes = return_volume_types_get_all_types(None)
|
||||
text = serializer.serialize({'volume_types': vtypes.values()})
|
||||
|
||||
print text
|
||||
tree = etree.fromstring(text)
|
||||
|
||||
self.assertEqual('volume_types', tree.tag)
|
||||
@ -158,7 +141,6 @@ class VolumeTypesSerializerTest(test.TestCase):
|
||||
vtype = stub_volume_type(1)
|
||||
text = serializer.serialize(dict(volume_type=vtype))
|
||||
|
||||
print text
|
||||
tree = etree.fromstring(text)
|
||||
|
||||
self._verify_volume_type(vtype, tree)
|
||||
|
@ -116,7 +116,6 @@
|
||||
"compute_extension:zones": [],
|
||||
|
||||
|
||||
|
||||
"volume:create": [],
|
||||
"volume:get": [],
|
||||
"volume:get_all": [],
|
||||
@ -125,7 +124,6 @@
|
||||
"volume:update": [],
|
||||
"volume:delete_volume_metadata": [],
|
||||
"volume:update_volume_metadata": [],
|
||||
|
||||
"volume:attach": [],
|
||||
"volume:detach": [],
|
||||
"volume:reserve_volume": [],
|
||||
@ -134,13 +132,16 @@
|
||||
"volume:check_detach": [],
|
||||
"volume:initialize_connection": [],
|
||||
"volume:terminate_connection": [],
|
||||
|
||||
"volume:create_snapshot": [],
|
||||
"volume:delete_snapshot": [],
|
||||
"volume:get_snapshot": [],
|
||||
"volume:get_all_snapshots": [],
|
||||
|
||||
|
||||
"volume_extension:types_manage": [],
|
||||
"volume_extension:types_extra_specs": [],
|
||||
|
||||
|
||||
"network:get_all_networks": [],
|
||||
"network:get_network": [],
|
||||
"network:delete_network": [],
|
||||
|
Loading…
Reference in New Issue
Block a user