Added API to manage volume types

Partially-implements bp volume-type-support

Change-Id: I1a58cf82c659b49e5258a563788cb8e5b05bfb8a
This commit is contained in:
Yulia Portnova 2014-04-17 10:18:35 +03:00 committed by Valeriy Ponomaryov
parent 9dfc2d22a6
commit dd4cbb9096
25 changed files with 1919 additions and 8 deletions

View File

@ -0,0 +1,100 @@
# Copyright 2014 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 tempest.api.share import base
from tempest.common.utils import data_utils
from tempest import exceptions
from tempest import test
class VolumeTypesAdminTest(base.BaseSharesAdminTest):
@test.attr(type=["gate", "smoke", ])
def test_volume_type_create_delete(self):
name = data_utils.rand_name("tempest-manila")
# Create volume type
resp, vt_create = self.shares_client.create_volume_type(name)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(name, vt_create['name'])
# Delete volume type
resp, __ = self.shares_client.delete_volume_type(vt_create['id'])
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
# Verify deletion of volume type
self.shares_client.wait_for_resource_deletion(vt_id=vt_create['id'])
self.assertRaises(exceptions.NotFound,
self.shares_client.get_volume_type,
vt_create['id'])
@test.attr(type=["gate", "smoke", ])
def test_volume_type_create_get(self):
name = data_utils.rand_name("tempest-manila")
extra_specs = {"key": "value", }
# Create volume type
resp, vt_create = self.create_volume_type(name,
extra_specs=extra_specs)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(name, vt_create['name'])
# Get volume type
resp, get = self.shares_client.get_volume_type(vt_create['id'])
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(name, get["name"])
self.assertEqual(vt_create["id"], get["id"])
self.assertEqual(extra_specs, get["extra_specs"])
@test.attr(type=["gate", "smoke", ])
def test_volume_type_create_list(self):
name = data_utils.rand_name("tempest-manila")
# Create volume type
resp, vt_create = self.create_volume_type(name)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
# list volume types
resp, vt_list = self.shares_client.list_volume_types()
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertTrue(len(vt_list) >= 1)
self.assertTrue(any(vt_create["id"] in vt["id"] for vt in vt_list))
@test.attr(type=["gate", "smoke", ])
def test_get_share_with_volume_type(self):
# Data
share_name = data_utils.rand_name("share")
vol_type_name = data_utils.rand_name("volume-type")
extra_specs = {"key": "value", }
# Create volume type
resp, vt_create = self.create_volume_type(vol_type_name,
extra_specs=extra_specs)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
# Create share with volume type
resp, share = self.create_share_wait_for_active(
name=share_name, volume_type_id=vt_create["id"])
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(share["name"], share_name)
self.shares_client.wait_for_share_status(share["id"], "available")
# Verify share info
resp, get = self.shares_client.get_share(share["id"])
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(share_name, get["name"])
self.assertEqual(share["id"], get["id"])
self.assertEqual(vol_type_name, get["volume_type"])

View File

@ -0,0 +1,145 @@
# Copyright 2014 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 tempest.api.share import base
from tempest.common.utils import data_utils
from tempest import test
class ExtraSpecsAdminTest(base.BaseSharesAdminTest):
@classmethod
def setUpClass(cls):
super(ExtraSpecsAdminTest, cls).setUpClass()
vol_type_name = data_utils.rand_name("volume-type")
__, cls.volume_type = cls.create_volume_type(vol_type_name)
@test.attr(type=["gate", "smoke", ])
def test_volume_type_extra_specs_list(self):
extra_specs = {
"key1": "value1",
"key2": "value2",
}
resp, es_create = self.shares_client.create_volume_type_extra_specs(
self.volume_type["id"], extra_specs)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(extra_specs, es_create)
resp, es_list = self.shares_client.list_volume_types_extra_specs(
self.volume_type["id"])
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(extra_specs, es_list)
@test.attr(type=["gate", "smoke", ])
def test_update_one_volume_type_extra_spec(self):
extra_specs = {
"key1": "value1",
"key2": "value2",
}
# Create extra specs for volume type
resp, es_create = self.shares_client.create_volume_type_extra_specs(
self.volume_type['id'], extra_specs)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(extra_specs, es_create)
# Update extra specs of volume type
extra_specs["key1"] = "fake_value1_updated"
resp, update_one = self.shares_client.update_volume_type_extra_spec(
self.volume_type["id"], "key1", extra_specs["key1"])
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual({"key1": extra_specs["key1"]}, update_one)
@test.attr(type=["gate", "smoke", ])
def test_update_all_volume_type_extra_specs(self):
extra_specs = {
"key1": "value1",
"key2": "value2",
}
# Create extra specs for volume type
resp, es_create = self.shares_client.create_volume_type_extra_specs(
self.volume_type['id'], extra_specs)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(extra_specs, es_create)
# Update extra specs of volume type
extra_specs["key2"] = "value2_updated"
resp, update_all = self.shares_client.update_volume_type_extra_specs(
self.volume_type["id"], extra_specs)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(extra_specs, update_all)
@test.attr(type=["gate", "smoke", ])
def test_get_all_volume_type_extra_specs(self):
extra_specs = {
"key1": "value1",
"key2": "value2",
}
# Create extra specs for volume type
resp, es_create = self.shares_client.create_volume_type_extra_specs(
self.volume_type['id'], extra_specs)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(extra_specs, es_create)
# Get all extra specs for volume type
resp, es_get_all = self.shares_client.get_volume_type_extra_specs(
self.volume_type["id"])
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(extra_specs, es_get_all)
@test.attr(type=["gate", "smoke", ])
def test_get_one_volume_type_extra_spec(self):
extra_specs = {
"key1": "value1",
"key2": "value2",
}
# Create extra specs for volume type
resp, es_create = self.shares_client.create_volume_type_extra_specs(
self.volume_type['id'], extra_specs)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(extra_specs, es_create)
# Get one extra spec for volume type
resp, es_get_one = self.shares_client.get_volume_type_extra_spec(
self.volume_type["id"], "key1")
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual({"key1": "value1", }, es_get_one)
@test.attr(type=["gate", "smoke", ])
def test_delete_one_volume_type_extra_spec(self):
extra_specs = {
"key1": "value1",
"key2": "value2",
}
# Create extra specs for volume type
resp, es_create = self.shares_client.create_volume_type_extra_specs(
self.volume_type['id'], extra_specs)
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual(extra_specs, es_create)
# Delete one extra spec for volume type
resp, __ = self.shares_client.delete_volume_type_extra_spec(
self.volume_type["id"], "key1")
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
# Get all extra specs for volume type
resp, es_get_all = self.shares_client.get_volume_type_extra_specs(
self.volume_type["id"])
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertEqual({"key2": "value2", }, es_get_all)

View File

@ -0,0 +1,233 @@
# Copyright 2014 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 tempest.api.share import base
from tempest import clients_share as clients
from tempest.common.utils import data_utils
from tempest import exceptions
from tempest import test
class ExtraSpecsAdminNegativeTest(base.BaseSharesAdminTest):
def _create_volume_type(self):
name = data_utils.rand_name("unique_vt_name")
extra_specs = {"key": "value", }
__, vt = self.create_volume_type(name, extra_specs=extra_specs)
return vt
@classmethod
def setUpClass(cls):
super(ExtraSpecsAdminNegativeTest, cls).setUpClass()
cls.member_shares_client = clients.Manager().shares_client
@test.attr(type=["gate", "smoke", ])
def test_try_create_extra_specs_with_user(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.Unauthorized,
self.member_shares_client.create_volume_type_extra_specs,
vt["id"], {"key": "new_value"})
@test.attr(type=["gate", "smoke", ])
def test_try_list_extra_specs_with_user(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.Unauthorized,
self.member_shares_client.list_volume_types_extra_specs, vt["id"])
@test.attr(type=["gate", "smoke", ])
def test_try_get_extra_spec_with_user(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.Unauthorized,
self.member_shares_client.get_volume_type_extra_spec,
vt["id"], "key")
@test.attr(type=["gate", "smoke", ])
def test_try_get_extra_specs_with_user(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.Unauthorized,
self.member_shares_client.get_volume_type_extra_specs, vt["id"])
@test.attr(type=["gate", "smoke", ])
def test_try_update_extra_spec_with_user(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.Unauthorized,
self.member_shares_client.update_volume_type_extra_spec,
vt["id"], "key", "new_value")
@test.attr(type=["gate", "smoke", ])
def test_try_update_extra_specs_with_user(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.Unauthorized,
self.member_shares_client.update_volume_type_extra_specs,
vt["id"], {"key": "new_value"})
@test.attr(type=["gate", "smoke", ])
def test_try_delete_extra_specs_with_user(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.Unauthorized,
self.member_shares_client.delete_volume_type_extra_spec,
vt["id"], "key")
@test.attr(type=["gate", "smoke", ])
def test_try_set_too_long_key(self):
too_big_key = "k" * 256
vt = self._create_volume_type()
self.assertRaises(exceptions.BadRequest,
self.shares_client.create_volume_type_extra_specs,
vt["id"], {too_big_key: "value"})
@test.attr(type=["gate", "smoke", ])
def test_try_set_too_long_value_with_creation(self):
too_big_value = "v" * 256
vt = self._create_volume_type()
self.assertRaises(exceptions.BadRequest,
self.shares_client.create_volume_type_extra_specs,
vt["id"], {"key": too_big_value})
@test.attr(type=["gate", "smoke", ])
def test_try_set_too_long_value_with_update(self):
too_big_value = "v" * 256
vt = self._create_volume_type()
resp, body = self.shares_client.create_volume_type_extra_specs(
vt["id"], {"key": "value"})
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertRaises(exceptions.BadRequest,
self.shares_client.update_volume_type_extra_specs,
vt["id"], {"key": too_big_value})
@test.attr(type=["gate", "smoke", ])
def test_try_set_too_long_value_with_update_of_one_key(self):
too_big_value = "v" * 256
vt = self._create_volume_type()
resp, body = self.shares_client.create_volume_type_extra_specs(
vt["id"], {"key": "value"})
self.assertIn(int(resp["status"]), test.HTTP_SUCCESS)
self.assertRaises(exceptions.BadRequest,
self.shares_client.update_volume_type_extra_spec,
vt["id"], "key", too_big_value)
@test.attr(type=["gate", "smoke", ])
def test_try_list_es_with_empty_vol_type_id(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.list_volume_types_extra_specs, "")
@test.attr(type=["gate", "smoke", ])
def test_try_list_es_with_invalid_vol_type_id(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.list_volume_types_extra_specs,
data_utils.rand_name("fake"))
@test.attr(type=["gate", "smoke", ])
def test_try_create_es_with_empty_vol_type_id(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.create_volume_type_extra_specs,
"", {"key1": "value1", })
@test.attr(type=["gate", "smoke", ])
def test_try_create_es_with_invalid_vol_type_id(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.create_volume_type_extra_specs,
data_utils.rand_name("fake"), {"key1": "value1", })
@test.attr(type=["gate", "smoke", ])
def test_try_create_es_with_empty_specs(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.BadRequest,
self.shares_client.create_volume_type_extra_specs,
vt["id"], "")
@test.attr(type=["gate", "smoke", ])
def test_try_create_es_with_invalid_specs(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.BadRequest,
self.shares_client.create_volume_type_extra_specs,
vt["id"], {"": "value_with_empty_key"})
@test.attr(type=["gate", "smoke", ])
def test_try_get_extra_spec_with_empty_key(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.NotFound,
self.shares_client.get_volume_type_extra_spec,
vt["id"], "")
@test.attr(type=["gate", "smoke", ])
def test_try_get_extra_spec_with_invalid_key(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.NotFound,
self.shares_client.get_volume_type_extra_spec,
vt["id"], data_utils.rand_name("fake"))
@test.attr(type=["gate", "smoke", ])
def test_try_get_extra_specs_with_empty_vol_type_id(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.get_volume_type_extra_specs,
"")
@test.attr(type=["gate", "smoke", ])
def test_try_get_extra_specs_with_invalid_vol_type_id(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.get_volume_type_extra_specs,
data_utils.rand_name("fake"))
@test.attr(type=["gate", "smoke", ])
def test_try_delete_es_key_with_empty_vol_type_id(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.delete_volume_type_extra_spec,
"", "key", )
@test.attr(type=["gate", "smoke", ])
def test_try_delete_es_key_with_invalid_vol_type_id(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.delete_volume_type_extra_spec,
data_utils.rand_name("fake"), "key", )
@test.attr(type=["gate", "smoke", ])
def test_try_delete_with_invalid_key(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.NotFound,
self.shares_client.delete_volume_type_extra_spec,
vt["id"], data_utils.rand_name("fake"))
@test.attr(type=["gate", "smoke", ])
def test_try_update_spec_with_empty_vol_type_id(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.update_volume_type_extra_spec,
"", "key", "new_value")
@test.attr(type=["gate", "smoke", ])
def test_try_update_spec_with_invalid_vol_type_id(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.update_volume_type_extra_spec,
data_utils.rand_name("fake"), "key", "new_value")
@test.attr(type=["gate", "smoke", ])
def test_try_update_spec_with_empty_key(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.NotFound,
self.shares_client.update_volume_type_extra_spec,
vt["id"], "", "new_value")
@test.attr(type=["gate", "smoke", ])
def test_try_update_with_invalid_vol_type_id(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.update_volume_type_extra_specs,
data_utils.rand_name("fake"), {"key": "new_value"})
@test.attr(type=["gate", "smoke", ])
def test_try_update_with_invalid_specs(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.BadRequest,
self.shares_client.update_volume_type_extra_specs,
vt["id"], {"": "new_value"})

View File

@ -0,0 +1,83 @@
# Copyright 2014 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 tempest.api.share import base
from tempest import clients_share as clients
from tempest.common.utils import data_utils
from tempest import exceptions
from tempest import test
class VolumeTypesAdminNegativeTest(base.BaseSharesAdminTest):
def _create_volume_type(self):
name = data_utils.rand_name("unique_vt_name")
extra_specs = {"key": "value", }
__, vt = self.create_volume_type(name, extra_specs=extra_specs)
return vt
@classmethod
def setUpClass(cls):
super(VolumeTypesAdminNegativeTest, cls).setUpClass()
cls.member_shares_client = clients.Manager().shares_client
@test.attr(type=["gate", "smoke", ])
def test_try_create_volume_type_with_user(self):
self.assertRaises(exceptions.Unauthorized,
self.create_volume_type,
data_utils.rand_name("used_user_creds"),
client=self.member_shares_client)
@test.attr(type=["gate", "smoke", ])
def test_try_delete_volume_type_with_user(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.Unauthorized,
self.member_shares_client.delete_volume_type,
vt["id"])
@test.attr(type=["gate", "smoke", ])
def test_create_share_with_nonexistent_volume_type(self):
self.assertRaises(exceptions.NotFound,
self.create_share_wait_for_active,
volume_type_id=data_utils.rand_name("fake"))
@test.attr(type=["gate", "smoke", ])
def test_create_volume_type_with_empty_name(self):
self.assertRaises(exceptions.BadRequest, self.create_volume_type, '')
@test.attr(type=["gate", "smoke", ])
def test_create_volume_type_with_too_big_name(self):
self.assertRaises(exceptions.BadRequest,
self.create_volume_type,
"x" * 256)
@test.attr(type=["gate", "smoke", ])
def test_get_volume_type_by_nonexistent_id(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.get_volume_type,
data_utils.rand_name("fake"))
@test.attr(type=["gate", "smoke", ])
def test_try_delete_volume_type_by_nonexistent_id(self):
self.assertRaises(exceptions.NotFound,
self.shares_client.delete_volume_type,
data_utils.rand_name("fake"))
@test.attr(type=["gate", "smoke", ])
def test_try_create_duplicate_of_volume_type(self):
vt = self._create_volume_type()
self.assertRaises(exceptions.Conflict,
self.create_volume_type,
vt["name"])

View File

@ -231,7 +231,9 @@ class BaseSharesTest(test.BaseTestCase):
def create_share_wait_for_active(cls, share_protocol=None, size=1,
name=None, snapshot_id=None,
description=None, metadata={},
share_network_id=None, client=None,
share_network_id=None,
volume_type_id=None,
client=None,
cleanup_in_class=True):
if client is None:
client = cls.shares_client
@ -249,7 +251,8 @@ class BaseSharesTest(test.BaseTestCase):
name=name, snapshot_id=snapshot_id,
description=description,
metadata=metadata,
share_network_id=share_network_id)
share_network_id=share_network_id,
volume_type_id=volume_type_id, )
resource = {
"type": "share",
"id": s["id"],
@ -317,6 +320,23 @@ class BaseSharesTest(test.BaseTestCase):
cls.method_resources.insert(0, resource)
return resp, ss
@classmethod
def create_volume_type(cls, name, client=None, cleanup_in_class=True,
**kwargs):
if client is None:
client = cls.shares_client
resp, vt = client.create_volume_type(name, **kwargs)
resource = {
"type": "volume_type",
"id": vt["id"],
"client": client,
}
if cleanup_in_class:
cls.class_resources.insert(0, resource)
else:
cls.method_resources.insert(0, resource)
return resp, vt
@classmethod
def clear_isolated_creds(cls, creds=None):
if creds is None:
@ -368,6 +388,9 @@ class BaseSharesTest(test.BaseTestCase):
elif res["type"] is "security_service":
client.delete_security_service(res_id)
client.wait_for_resource_deletion(ss_id=res_id)
elif res["type"] is "volume_type":
client.delete_volume_type(res_id)
client.wait_for_resource_deletion(vt_id=res_id)
except exceptions.NotFound:
pass
except exceptions.Unauthorized:

View File

@ -49,7 +49,8 @@ class SharesClient(rest_client.RestClient):
def create_share(self, share_protocol=None, size=1,
name=None, snapshot_id=None, description=None,
metadata={}, share_network_id=None):
metadata={}, share_network_id=None,
volume_type_id=None, ):
if name is None:
name = rand_name("tempest-created-share")
if description is None:
@ -70,6 +71,8 @@ class SharesClient(rest_client.RestClient):
}
if share_network_id:
post_body["share"]["share_network_id"] = share_network_id
if volume_type_id:
post_body["share"]["volume_type"] = volume_type_id
body = json.dumps(post_body)
resp, body = self.post("shares", body)
return resp, self._parse_resp(body)
@ -267,7 +270,7 @@ class SharesClient(rest_client.RestClient):
"""Verifies deleted resource or not.
:param kwargs: expected keys are 'share_id', 'rule_id',
:param kwargs: 'snapshot_id', 'sn_id', 'ss_id'
:param kwargs: 'snapshot_id', 'sn_id', 'ss_id' and 'vt_id'
:raises share_exceptions.InvalidResource
"""
@ -307,6 +310,11 @@ class SharesClient(rest_client.RestClient):
self.get_security_service(kwargs.get("sn_id"))
except exceptions.NotFound:
return True
elif "vt_id" in kwargs:
try:
self.get_volume_type(kwargs.get("vt_id"))
except exceptions.NotFound:
return True
else:
raise share_exceptions.InvalidResource(message=str(kwargs))
return False
@ -529,3 +537,72 @@ class SharesClient(rest_client.RestClient):
def list_sec_services_for_share_network(self, sn_id):
resp, body = self.get("security-services?share_network_id=%s" % sn_id)
return resp, self._parse_resp(body)
###############
def list_volume_types(self, params=None):
uri = 'types'
if params is not None:
uri += '?%s' % urllib.urlencode(params)
resp, body = self.get(uri)
return resp, self._parse_resp(body)
def create_volume_type(self, name, **kwargs):
post_body = {
'name': name,
'extra_specs': kwargs.get('extra_specs'),
}
post_body = json.dumps({'volume_type': post_body})
resp, body = self.post('types', post_body)
return resp, self._parse_resp(body)
def delete_volume_type(self, vol_type_id):
return self.delete("types/%s" % vol_type_id)
def get_volume_type(self, vol_type_id):
resp, body = self.get("types/%s" % vol_type_id)
return resp, self._parse_resp(body)
###############
def list_volume_types_extra_specs(self, vol_type_id, params=None):
uri = 'types/%s/extra_specs' % vol_type_id
if params is not None:
uri += '?%s' % urllib.urlencode(params)
resp, body = self.get(uri)
return resp, self._parse_resp(body)
def create_volume_type_extra_specs(self, vol_type_id, extra_specs):
url = "types/%s/extra_specs" % vol_type_id
post_body = json.dumps({'extra_specs': extra_specs})
resp, body = self.post(url, post_body)
return resp, self._parse_resp(body)
def get_volume_type_extra_spec(self, vol_type_id, extra_spec_name):
uri = "types/%s/extra_specs/%s" % (vol_type_id, extra_spec_name)
resp, body = self.get(uri)
return resp, self._parse_resp(body)
def get_volume_type_extra_specs(self, vol_type_id):
uri = "types/%s/extra_specs" % vol_type_id
resp, body = self.get(uri)
return resp, self._parse_resp(body)
def update_volume_type_extra_spec(self, vol_type_id, spec_name,
spec_value):
uri = "types/%s/extra_specs/%s" % (vol_type_id, spec_name)
extra_spec = {spec_name: spec_value}
post_body = json.dumps(extra_spec)
resp, body = self.put(uri, post_body)
return resp, self._parse_resp(body)
def update_volume_type_extra_specs(self, vol_type_id, extra_specs):
uri = "types/%s/extra_specs" % vol_type_id
extra_specs = {"extra_specs": extra_specs}
post_body = json.dumps(extra_specs)
resp, body = self.post(uri, post_body)
return resp, self._parse_resp(body)
def delete_volume_type_extra_spec(self, vol_type_id, extra_spec_name):
uri = "types/%s/extra_specs/%s" % (vol_type_id, extra_spec_name)
return self.delete(uri)

View File

@ -20,6 +20,9 @@
"share_extension:services": [["rule:admin_api"]],
"share_extension:types_manage": [["rule:admin_api"]],
"share_extension:types_extra_specs": [["rule:admin_api"]],
"security_service:create": [],
"security_service:delete": [],
"security_service:update": [],

View File

@ -35,6 +35,22 @@ CONF = cfg.CONF
XML_NS_V1 = 'http://docs.openstack.org/volume/api/v1'
# Regex that matches alphanumeric characters, periods, hypens,
# colons and underscores:
# ^ assert position at start of the string
# [\w\.\-\:\_] match expression
# $ assert position at end of the string
VALID_KEY_NAME_REGEX = re.compile(r"^[\w\.\-\:\_]+$", re.UNICODE)
def validate_key_names(key_names_list):
"""Validate each item of the list to match key name regex."""
for key_name in key_names_list:
if not VALID_KEY_NAME_REGEX.match(key_name):
return False
return True
def get_pagination_params(request):
"""Return marker, limit tuple from request.

View File

@ -0,0 +1,172 @@
# Copyright (c) 2011 Zadara Storage Inc.
# Copyright (c) 2011 OpenStack Foundation
#
# 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 manila.api import common
from manila.api import extensions
from manila.api.openstack import wsgi
from manila import db
from manila import exception
from manila.openstack.common.notifier import api as notifier_api
from manila.share import volume_types
authorize = extensions.extension_authorizer('share', 'types_extra_specs')
class VolumeTypeExtraSpecsController(wsgi.Controller):
"""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_type(self, context, type_id):
try:
volume_types.get_volume_type(context, type_id)
except exception.NotFound as ex:
raise webob.exc.HTTPNotFound(explanation=ex.msg)
def _verify_extra_specs(self, extra_specs):
# keys and values in extra_specs can be only strings
# with length in range(1, 256)
is_valid = True
for k, v in extra_specs.iteritems():
if not (isinstance(k, basestring) and len(k) in range(1, 256)):
is_valid = False
break
if isinstance(v, dict):
self._verify_extra_specs(v)
elif isinstance(v, basestring):
if len(v) not in range(1, 256):
is_valid = False
break
else:
is_valid = False
break
if not is_valid:
expl = _('Invalid request body')
raise webob.exc.HTTPBadRequest(explanation=expl)
def index(self, req, type_id):
"""Returns the list of extra specs for a given volume type."""
context = req.environ['manila.context']
authorize(context)
self._check_type(context, type_id)
return self._get_extra_specs(context, type_id)
def create(self, req, type_id, body=None):
context = req.environ['manila.context']
authorize(context)
if not self.is_valid_body(body, 'extra_specs'):
raise webob.exc.HTTPBadRequest()
self._check_type(context, type_id)
self._verify_extra_specs(body)
specs = body['extra_specs']
self._check_key_names(specs.keys())
db.volume_type_extra_specs_update_or_create(context,
type_id,
specs)
notifier_info = dict(type_id=type_id, specs=specs)
notifier_api.notify(context, 'volumeTypeExtraSpecs',
'volume_type_extra_specs.create',
notifier_api.INFO, notifier_info)
return body
def update(self, req, type_id, id, body=None):
context = req.environ['manila.context']
authorize(context)
if not body:
expl = _('Request body empty')
raise webob.exc.HTTPBadRequest(explanation=expl)
self._check_type(context, type_id)
if id not 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)
self._verify_extra_specs(body)
db.volume_type_extra_specs_update_or_create(context,
type_id,
body)
notifier_info = dict(type_id=type_id, id=id)
notifier_api.notify(context, 'volumeTypeExtraSpecs',
'volume_type_extra_specs.update',
notifier_api.INFO, notifier_info)
return body
def show(self, req, type_id, id):
"""Return a single extra spec item."""
context = req.environ['manila.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['manila.context']
self._check_type(context, type_id)
authorize(context)
try:
db.volume_type_extra_specs_delete(context, type_id, id)
except exception.VolumeTypeExtraSpecsNotFound as error:
raise webob.exc.HTTPNotFound(explanation=error.msg)
notifier_info = dict(type_id=type_id, id=id)
notifier_api.notify(context, 'volumeTypeExtraSpecs',
'volume_type_extra_specs.delete',
notifier_api.INFO, notifier_info)
return webob.Response(status_int=202)
def _check_key_names(self, keys):
if not common.validate_key_names(keys):
expl = _('Key names can only contain alphanumeric characters, '
'underscores, periods, colons and hyphens.')
raise webob.exc.HTTPBadRequest(explanation=expl)
class Types_extra_specs(extensions.ExtensionDescriptor):
"""Type extra specs support."""
name = "TypesExtraSpecs"
alias = "os-types-extra-specs"
namespace = "http://docs.openstack.org/share/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

View File

@ -0,0 +1,124 @@
# Copyright (c) 2011 OpenStack Foundation
#
# 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 manila.api import extensions
from manila.api.openstack import wsgi
from manila.api.views import types as views_types
from manila import exception
from manila.openstack.common.notifier import api as notifier_api
from manila.share import volume_types
authorize = extensions.extension_authorizer('share', 'types_manage')
class VolumeTypesManageController(wsgi.Controller):
"""The volume types API controller for the OpenStack API."""
_view_builder_class = views_types.ViewBuilder
def _notify_volume_type_error(self, context, method, payload):
notifier_api.notify(context,
'volumeType',
method,
notifier_api.ERROR,
payload)
@wsgi.action("create")
def _create(self, req, body):
"""Creates a new volume type."""
context = req.environ['manila.context']
authorize(context)
if not self.is_valid_body(body, 'volume_type'):
raise webob.exc.HTTPBadRequest()
vol_type = body['volume_type']
name = vol_type.get('name', None)
specs = vol_type.get('extra_specs', {})
if name is None or name == "" or len(name) > 255:
raise webob.exc.HTTPBadRequest()
try:
volume_types.create(context, name, specs)
vol_type = volume_types.get_volume_type_by_name(context, name)
notifier_info = dict(volume_types=vol_type)
notifier_api.notify(context, 'volumeType',
'volume_type.create',
notifier_api.INFO, notifier_info)
except exception.VolumeTypeExists as err:
notifier_err = dict(volume_types=vol_type, error_message=str(err))
self._notify_volume_type_error(context,
'volume_type.create',
notifier_err)
raise webob.exc.HTTPConflict(explanation=str(err))
except exception.NotFound as err:
notifier_err = dict(volume_types=vol_type, error_message=str(err))
self._notify_volume_type_error(context,
'volume_type.create',
notifier_err)
raise webob.exc.HTTPNotFound()
return self._view_builder.show(req, vol_type)
@wsgi.action("delete")
def _delete(self, req, id):
"""Deletes an existing volume type."""
context = req.environ['manila.context']
authorize(context)
try:
vol_type = volume_types.get_volume_type(context, id)
volume_types.destroy(context, vol_type['id'])
notifier_info = dict(volume_types=vol_type)
notifier_api.notify(context, 'volumeType',
'volume_type.delete',
notifier_api.INFO, notifier_info)
except exception.VolumeTypeInUse as err:
notifier_err = dict(id=id, error_message=str(err))
self._notify_volume_type_error(context,
'volume_type.delete',
notifier_err)
msg = 'Target volume type is still in use.'
raise webob.exc.HTTPBadRequest(explanation=msg)
except exception.NotFound as err:
notifier_err = dict(id=id, error_message=str(err))
self._notify_volume_type_error(context,
'volume_type.delete',
notifier_err)
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/share/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]

View File

@ -31,6 +31,7 @@ from manila.api.v1 import share_metadata
from manila.api.v1 import share_networks
from manila.api.v1 import share_snapshots
from manila.api.v1 import shares
from manila.api.v1 import volume_types
from manila.openstack.common import log as logging
@ -95,3 +96,9 @@ class APIRouter(manila.api.openstack.APIRouter):
controller=self.resources['share_networks'],
collection={'detail': 'GET'},
member={'action': 'POST'})
self.resources['types'] = volume_types.create_resource()
mapper.resource("type", "types",
controller=self.resources['types'],
collection={'detail': 'GET'},
member={'action': 'POST'})

View File

@ -25,7 +25,9 @@ from manila.api import xmlutil
from manila.common import constants
from manila import exception
from manila.openstack.common import log as logging
from manila.openstack.common import uuidutils
from manila import share
from manila.share import volume_types
LOG = logging.getLogger(__name__)
@ -217,6 +219,21 @@ class ShareController(wsgi.Controller):
display_name = share.get('display_name')
display_description = share.get('display_description')
req_volume_type = share.get('volume_type', None)
if req_volume_type:
try:
if not uuidutils.is_uuid_like(req_volume_type):
kwargs['volume_type'] = \
volume_types.get_volume_type_by_name(
context, req_volume_type)
else:
kwargs['volume_type'] = volume_types.get_volume_type(
context, req_volume_type)
except exception.VolumeTypeNotFound:
msg = _("Volume type not found.")
raise exc.HTTPNotFound(explanation=msg)
new_share = self.share_api.create(context,
share_proto,
size,

View File

@ -0,0 +1,51 @@
# Copyright (c) 2014 NetApp, Inc.
#
# 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 type & volume types extra specs extension."""
from webob import exc
from manila.api.openstack import wsgi
from manila.api.views import types as views_types
from manila import exception
from manila.share import volume_types
class VolumeTypesController(wsgi.Controller):
"""The volume types API controller for the OpenStack API."""
_view_builder_class = views_types.ViewBuilder
def index(self, req):
"""Returns the list of volume types."""
context = req.environ['manila.context']
vol_types = volume_types.get_all_types(context).values()
return self._view_builder.index(req, vol_types)
def show(self, req, id):
"""Return a single volume type item."""
context = req.environ['manila.context']
try:
vol_type = volume_types.get_volume_type(context, id)
except exception.NotFound:
msg = _("Volume type not found")
raise exc.HTTPNotFound(explanation=msg)
vol_type['id'] = str(vol_type['id'])
return self._view_builder.show(req, vol_type)
def create_resource():
return wsgi.Resource(VolumeTypesController())

View File

@ -49,6 +49,10 @@ class ViewBuilder(common.ViewBuilder):
metadata = dict((item['key'], item['value']) for item in metadata)
else:
metadata = {}
if share['volume_type_id'] and share.get('volume_type'):
volume_type = share['volume_type']['name']
else:
volume_type = share['volume_type_id']
return {
'share': {
@ -66,6 +70,7 @@ class ViewBuilder(common.ViewBuilder):
'share_proto': share.get('share_proto'),
'export_location': share.get('export_location'),
'metadata': metadata,
'volume_type': volume_type,
'links': self._get_links(request, share['id'])
}
}

34
manila/api/views/types.py Normal file
View File

@ -0,0 +1,34 @@
# Copyright 2012 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 manila.api import common
class ViewBuilder(common.ViewBuilder):
_collection_name = 'types'
def show(self, request, volume_type, brief=False):
"""Trim away extraneous volume type attributes."""
trimmed = dict(id=volume_type.get('id'),
name=volume_type.get('name'),
extra_specs=volume_type.get('extra_specs'))
return trimmed if brief else dict(volume_type=trimmed)
def index(self, request, volume_types):
"""Index over trimmed volume types."""
volume_types_list = [self.show(request, volume_type, True)
for volume_type in volume_types]
return dict(volume_types=volume_types_list)

View File

@ -115,8 +115,8 @@ class FilterScheduler(driver.Scheduler):
# takes 'resource_XX' and 'volume_XX' as input respectively, copying
# 'volume_XX' to 'resource_XX' will make both filters happy.
resource_properties = share_properties.copy()
share_type = request_spec.get("share_type", {})
resource_type = request_spec.get("share_type", {})
volume_type = request_spec.get("volume_type", {})
resource_type = request_spec.get("volume_type", {})
request_spec.update({'resource_properties': resource_properties})
config_options = self._get_configuration_options()
@ -128,7 +128,7 @@ class FilterScheduler(driver.Scheduler):
filter_properties.update({'context': context,
'request_spec': request_spec,
'config_options': config_options,
'share_type': share_type,
'volume_type': volume_type,
'resource_type': resource_type
})

View File

@ -50,7 +50,7 @@ class API(base.Base):
def create(self, context, share_proto, size, name, description,
snapshot=None, availability_zone=None, metadata=None,
share_network_id=None):
share_network_id=None, volume_type=None):
"""Create new share."""
policy.check_policy(context, 'share', 'create')
@ -86,6 +86,13 @@ class API(base.Base):
"than snapshot size") % size)
raise exception.InvalidInput(reason=msg)
if snapshot and volume_type:
if volume_type['id'] != snapshot['volume_type_id']:
msg = _("Invalid volume_type provided (requested type "
"must match source snapshot, or be omitted). "
"You should omit the argument.")
raise exception.InvalidInput(reason=msg)
#TODO(rushiagr): Find a suitable place to keep all the allowed
# share types so that it becomes easier to add one
if share_proto.lower() not in ['nfs', 'cifs']:
@ -134,6 +141,7 @@ class API(base.Base):
'display_name': name,
'display_description': description,
'share_proto': share_proto,
'volume_type_id': volume_type['id'] if volume_type else None
}
try:
@ -150,6 +158,7 @@ class API(base.Base):
'share_proto': share_proto,
'share_id': share['id'],
'snapshot_id': share['snapshot_id'],
'volume_type': volume_type
}
filter_properties = {}

View File

@ -0,0 +1,181 @@
# Copyright (c) 2014 Openstack Foundation.
#
# 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.
"""Built-in volume type properties."""
from oslo.config import cfg
from manila import context
from manila import db
from manila import exception
from manila.openstack.common import log as logging
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
def create(context, name, extra_specs={}):
"""Creates volume types."""
try:
type_ref = db.volume_type_create(context,
dict(name=name,
extra_specs=extra_specs))
except exception.DBError as e:
LOG.exception(_('DB error: %s') % e)
raise exception.VolumeTypeCreateFailed(name=name,
extra_specs=extra_specs)
return type_ref
def destroy(context, id):
"""Marks volume types as deleted."""
if id is None:
msg = _("id cannot be None")
raise exception.InvalidVolumeType(reason=msg)
else:
db.volume_type_destroy(context, id)
def get_all_types(context, inactive=0, search_opts={}):
"""Get all non-deleted volume_types.
Pass true as argument if you want deleted volume types returned also.
"""
vol_types = db.volume_type_get_all(context, inactive)
if search_opts:
LOG.debug(_("Searching by: %s") % search_opts)
def _check_extra_specs_match(vol_type, searchdict):
for k, v in searchdict.iteritems():
if (k not in vol_type['extra_specs'].keys()
or vol_type['extra_specs'][k] != v):
return False
return True
# search_option to filter_name mapping.
filter_mapping = {'extra_specs': _check_extra_specs_match}
result = {}
for type_name, type_args in vol_types.iteritems():
# go over all filters in the list
for opt, values in search_opts.iteritems():
try:
filter_func = filter_mapping[opt]
except KeyError:
# no such filter - ignore it, go to next filter
continue
else:
if filter_func(type_args, values):
result[type_name] = type_args
break
vol_types = result
return vol_types
def get_volume_type(ctxt, id):
"""Retrieves single volume type by id."""
if id is None:
msg = _("id cannot be None")
raise exception.InvalidVolumeType(reason=msg)
if ctxt is None:
ctxt = context.get_admin_context()
return db.volume_type_get(ctxt, id)
def get_volume_type_by_name(context, name):
"""Retrieves single volume type by name."""
if name is None:
msg = _("name cannot be None")
raise exception.InvalidVolumeType(reason=msg)
return db.volume_type_get_by_name(context, name)
def get_default_volume_type():
"""Get the default volume type."""
name = CONF.default_volume_type
vol_type = {}
if name is not None:
ctxt = context.get_admin_context()
try:
vol_type = get_volume_type_by_name(ctxt, name)
except exception.VolumeTypeNotFoundByName as e:
# Couldn't find volume type with the name in default_volume_type
# flag, record this issue and move on
#TODO(zhiteng) consider add notification to warn admin
LOG.exception(_('Default volume type is not found, '
'please check default_volume_type config: %s'), e)
return vol_type
def get_volume_type_extra_specs(volume_type_id, key=False):
volume_type = get_volume_type(context.get_admin_context(),
volume_type_id)
extra_specs = volume_type['extra_specs']
if key:
if extra_specs.get(key):
return extra_specs.get(key)
else:
return False
else:
return extra_specs
def volume_types_diff(context, vol_type_id1, vol_type_id2):
"""Returns a 'diff' of two volume types and whether they are equal.
Returns a tuple of (diff, equal), where 'equal' is a boolean indicating
whether there is any difference, and 'diff' is a dictionary with the
following format:
{'extra_specs': {'key1': (value_in_1st_vol_type, value_in_2nd_vol_type),
'key2': (value_in_1st_vol_type, value_in_2nd_vol_type),
...}
"""
def _dict_diff(dict1, dict2):
res = {}
equal = True
if dict1 is None:
dict1 = {}
if dict2 is None:
dict2 = {}
for k, v in dict1.iteritems():
res[k] = (v, dict2.get(k))
if k not in dict2 or res[k][0] != res[k][1]:
equal = False
for k, v in dict2.iteritems():
res[k] = (dict1.get(k), v)
if k not in dict1 or res[k][0] != res[k][1]:
equal = False
return (res, equal)
all_equal = True
diff = {}
vol_type1 = get_volume_type(context, vol_type_id1)
vol_type2 = get_volume_type(context, vol_type_id2)
extra_specs1 = vol_type1.get('extra_specs')
extra_specs2 = vol_type2.get('extra_specs')
diff['extra_specs'], equal = _dict_diff(extra_specs1, extra_specs2)
if not equal:
all_equal = False
return (diff, all_equal)

View File

@ -39,6 +39,7 @@ def stub_share(id, **kwargs):
'display_description': 'displaydesc',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'snapshot_id': '2',
'volume_type_id': '1',
'share_network_id': None
}
share.update(kwargs)

View File

@ -0,0 +1,307 @@
# Copyright (c) 2011 Zadara Storage Inc.
# Copyright (c) 2011 OpenStack Foundation
# 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
import mock
from manila.api.contrib import types_extra_specs
from manila import exception
from manila.openstack.common.notifier import api as notifier_api
from manila.openstack.common.notifier import test_notifier
from manila import test
from manila.tests.api import fakes
import manila.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 delete_volume_type_extra_specs_not_found(context, volume_type_id, key):
raise exception.VolumeTypeExtraSpecsNotFound("Not Found")
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.flags(host='fake',
notification_driver=[test_notifier.__name__])
self.stubs.Set(manila.db, 'volume_type_get', volume_type_get)
self.api_path = '/v2/fake/os-volume-types/1/extra_specs'
self.controller = types_extra_specs.VolumeTypeExtraSpecsController()
"""to reset notifier drivers left over from other api/contrib tests"""
notifier_api._reset_drivers()
test_notifier.NOTIFICATIONS = []
#self.addCleanup(notifier_api._reset_drivers)
def test_index(self):
self.stubs.Set(manila.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(manila.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(manila.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(manila.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(manila.db, 'volume_type_extra_specs_delete',
delete_volume_type_extra_specs)
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
req = fakes.HTTPRequest.blank(self.api_path + '/key5')
self.controller.delete(req, 1, 'key5')
self.assertEqual(len(test_notifier.NOTIFICATIONS), 1)
def test_delete_not_found(self):
self.stubs.Set(manila.db, 'volume_type_extra_specs_delete',
delete_volume_type_extra_specs_not_found)
req = fakes.HTTPRequest.blank(self.api_path + '/key6')
self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
req, 1, 'key6')
def test_create(self):
self.stubs.Set(manila.db,
'volume_type_extra_specs_update_or_create',
return_create_volume_type_extra_specs)
body = {"extra_specs": {"key1": "value1"}}
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
req = fakes.HTTPRequest.blank(self.api_path)
res_dict = self.controller.create(req, 1, body)
self.assertEqual(len(test_notifier.NOTIFICATIONS), 1)
self.assertEqual('value1', res_dict['extra_specs']['key1'])
def test_create_with_too_small_key(self):
self.stubs.Set(manila.db,
'volume_type_extra_specs_update_or_create',
return_create_volume_type_extra_specs)
too_small_key = ""
body = {"extra_specs": {too_small_key: "value"}}
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
req = fakes.HTTPRequest.blank(self.api_path)
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create, req, 1, body)
def test_create_with_too_big_key(self):
self.stubs.Set(manila.db,
'volume_type_extra_specs_update_or_create',
return_create_volume_type_extra_specs)
too_big_key = "k" * 256
body = {"extra_specs": {too_big_key: "value"}}
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
req = fakes.HTTPRequest.blank(self.api_path)
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create, req, 1, body)
def test_create_with_too_small_value(self):
self.stubs.Set(manila.db,
'volume_type_extra_specs_update_or_create',
return_create_volume_type_extra_specs)
too_small_value = ""
body = {"extra_specs": {"key": too_small_value}}
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
req = fakes.HTTPRequest.blank(self.api_path)
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create, req, 1, body)
def test_create_with_too_big_value(self):
self.stubs.Set(manila.db,
'volume_type_extra_specs_update_or_create',
return_create_volume_type_extra_specs)
too_big_value = "v" * 256
body = {"extra_specs": {"key": too_big_value}}
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
req = fakes.HTTPRequest.blank(self.api_path)
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create, req, 1, body)
@mock.patch.object(manila.db, 'volume_type_extra_specs_update_or_create')
def test_create_key_allowed_chars(
self, volume_type_extra_specs_update_or_create):
mock_return_value = {"key1": "value1",
"key2": "value2",
"key3": "value3",
"key4": "value4",
"key5": "value5"}
volume_type_extra_specs_update_or_create.\
return_value = mock_return_value
body = {"extra_specs": {"other_alphanum.-_:": "value1"}}
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
req = fakes.HTTPRequest.blank(self.api_path)
res_dict = self.controller.create(req, 1, body)
self.assertEqual(len(test_notifier.NOTIFICATIONS), 1)
self.assertEqual('value1',
res_dict['extra_specs']['other_alphanum.-_:'])
@mock.patch.object(manila.db, 'volume_type_extra_specs_update_or_create')
def test_create_too_many_keys_allowed_chars(
self, volume_type_extra_specs_update_or_create):
mock_return_value = {"key1": "value1",
"key2": "value2",
"key3": "value3",
"key4": "value4",
"key5": "value5"}
volume_type_extra_specs_update_or_create.\
return_value = mock_return_value
body = {"extra_specs": {"other_alphanum.-_:": "value1",
"other2_alphanum.-_:": "value2",
"other3_alphanum.-_:": "value3"}}
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
req = fakes.HTTPRequest.blank(self.api_path)
res_dict = self.controller.create(req, 1, body)
self.assertEqual(len(test_notifier.NOTIFICATIONS), 1)
self.assertEqual('value1',
res_dict['extra_specs']['other_alphanum.-_:'])
self.assertEqual('value2',
res_dict['extra_specs']['other2_alphanum.-_:'])
self.assertEqual('value3',
res_dict['extra_specs']['other3_alphanum.-_:'])
def test_update_item(self):
self.stubs.Set(manila.db,
'volume_type_extra_specs_update_or_create',
return_create_volume_type_extra_specs)
body = {"key1": "value1"}
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
req = fakes.HTTPRequest.blank(self.api_path + '/key1')
res_dict = self.controller.update(req, 1, 'key1', body)
self.assertEqual(len(test_notifier.NOTIFICATIONS), 1)
self.assertEqual('value1', res_dict['key1'])
def test_update_item_too_many_keys(self):
self.stubs.Set(manila.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(manila.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)
def _extra_specs_empty_update(self, body):
req = fakes.HTTPRequest.blank('/v2/fake/types/1/extra_specs')
req.method = 'POST'
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update, req, '1', body)
def test_update_no_body(self):
self._extra_specs_empty_update(body=None)
def test_update_empty_body(self):
self._extra_specs_empty_update(body={})
def _extra_specs_create_bad_body(self, body):
req = fakes.HTTPRequest.blank('/v2/fake/types/1/extra_specs')
req.method = 'POST'
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create, req, '1', body)
def test_create_no_body(self):
self._extra_specs_create_bad_body(body=None)
def test_create_missing_volume(self):
body = {'foo': {'a': 'b'}}
self._extra_specs_create_bad_body(body=body)
def test_create_malformed_entity(self):
body = {'extra_specs': 'string'}
self._extra_specs_create_bad_body(body=body)
def test_create_invalid_key(self):
body = {"extra_specs": {"ke/y1": "value1"}}
self._extra_specs_create_bad_body(body=body)
def test_create_invalid_too_many_key(self):
body = {"key1": "value1", "ke/y2": "value2", "key3": "value3"}
self._extra_specs_create_bad_body(body=body)

View File

@ -0,0 +1,148 @@
# Copyright 2011 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 webob
from manila.api.contrib import types_manage
from manila import exception
from manila.openstack.common.notifier import api as notifier_api
from manila.openstack.common.notifier import test_notifier
from manila.share import volume_types
from manila import test
from manila.tests.api 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_with_volumes_destroy(context, id):
if id == "1":
raise exception.VolumeTypeInUse(volume_type_id=id)
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]))
def make_create_body(name):
return {
"volume_type": {
"name": name,
"extra_specs": {
"key": "value",
}
}
}
class VolumeTypesManageApiTest(test.TestCase):
def setUp(self):
super(VolumeTypesManageApiTest, self).setUp()
self.flags(host='fake',
notification_driver=[test_notifier.__name__])
self.controller = types_manage.VolumeTypesManageController()
"""to reset notifier drivers left over from other api/contrib tests"""
notifier_api._reset_drivers()
test_notifier.NOTIFICATIONS = []
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)
self.stubs.Set(volume_types, 'get_volume_type',
return_volume_types_get_volume_type)
self.stubs.Set(volume_types, 'destroy',
return_volume_types_destroy)
def test_volume_types_delete(self):
req = fakes.HTTPRequest.blank('/v2/fake/types/1')
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
self.controller._delete(req, 1)
self.assertEqual(len(test_notifier.NOTIFICATIONS), 1)
def test_volume_types_delete_not_found(self):
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
req = fakes.HTTPRequest.blank('/v2/fake/types/777')
self.assertRaises(webob.exc.HTTPNotFound, self.controller._delete,
req, '777')
self.assertEqual(len(test_notifier.NOTIFICATIONS), 1)
def test_volume_types_with_volumes_destroy(self):
req = fakes.HTTPRequest.blank('/v2/fake/types/1')
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
self.controller._delete(req, 1)
self.assertEqual(len(test_notifier.NOTIFICATIONS), 1)
def test_create(self):
body = make_create_body("volume_type_1")
req = fakes.HTTPRequest.blank('/v2/fake/types')
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
res_dict = self.controller._create(req, body)
self.assertEqual(len(test_notifier.NOTIFICATIONS), 1)
self.assertEqual(1, len(res_dict))
self.assertEqual('vol_type_1', res_dict['volume_type']['name'])
def test_create_with_too_small_name(self):
req = fakes.HTTPRequest.blank('/v2/fake/types')
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller._create, req, make_create_body(""))
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
def test_create_with_too_big_name(self):
req = fakes.HTTPRequest.blank('/v2/fake/types')
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
self.assertRaises(webob.exc.HTTPBadRequest, self.controller._create,
req, make_create_body("n" * 256))
self.assertEqual(len(test_notifier.NOTIFICATIONS), 0)
def _create_volume_type_bad_body(self, body):
req = fakes.HTTPRequest.blank('/v2/fake/types')
req.method = 'POST'
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller._create, req, body)
def test_create_no_body(self):
self._create_volume_type_bad_body(body=None)
def test_create_missing_volume(self):
self._create_volume_type_bad_body(body={'foo': {'a': 'b'}})
def test_create_malformed_entity(self):
self._create_volume_type_bad_body(body={'volume_type': 'string'})

View File

@ -149,6 +149,7 @@ class ShareApiTest(test.TestCase):
'snapshot_id': '2',
'share_network_id': None,
'status': 'fakestatus',
'volume_type': '1',
'links': [{'href': 'http://localhost/v1/fake/shares/1',
'rel': 'self'},
{'href': 'http://localhost/fake/shares/1',
@ -254,6 +255,7 @@ class ShareApiTest(test.TestCase):
'share_network_id': None,
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'size': 1,
'volume_type': '1',
'links': [
{
'href': 'http://localhost/v1/fake/shares/1',

View File

@ -0,0 +1,168 @@
# Copyright 2011 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 webob
from manila.api.v1 import volume_types as types
from manila.api.views import types as views_types
from manila import exception
from manila.openstack.common import timeutils
from manila.share import volume_types
from manila import test
from manila.tests.api 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_all_types(context):
return dict(
vol_type_1=stub_volume_type(1),
vol_type_2=stub_volume_type(2),
vol_type_3=stub_volume_type(3)
)
def return_empty_volume_types_get_all_types(context):
return {}
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_get_by_name(context, name):
if name == "777":
raise exception.VolumeTypeNotFoundByName(volume_type_name=name)
return stub_volume_type(int(name.split("_")[2]))
class VolumeTypesApiTest(test.TestCase):
def setUp(self):
super(VolumeTypesApiTest, self).setUp()
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/fake/types')
res_dict = self.controller.index(req)
self.assertEqual(3, len(res_dict['volume_types']))
expected_names = ['vol_type_1', 'vol_type_2', 'vol_type_3']
actual_names = map(lambda e: e['name'], res_dict['volume_types'])
self.assertEqual(set(actual_names), set(expected_names))
for entry in res_dict['volume_types']:
self.assertEqual('value1', entry['extra_specs']['key1'])
def test_volume_types_index_no_data(self):
self.stubs.Set(volume_types, 'get_all_types',
return_empty_volume_types_get_all_types)
req = fakes.HTTPRequest.blank('/v2/fake/types')
res_dict = self.controller.index(req)
self.assertEqual(0, len(res_dict['volume_types']))
def test_volume_types_show(self):
self.stubs.Set(volume_types, 'get_volume_type',
return_volume_types_get_volume_type)
req = fakes.HTTPRequest.blank('/v2/fake/types/1')
res_dict = self.controller.show(req, 1)
self.assertEqual(1, len(res_dict))
self.assertEqual('1', res_dict['volume_type']['id'])
self.assertEqual('vol_type_1', res_dict['volume_type']['name'])
def test_volume_types_show_not_found(self):
self.stubs.Set(volume_types, 'get_volume_type',
return_volume_types_get_volume_type)
req = fakes.HTTPRequest.blank('/v2/fake/types/777')
self.assertRaises(webob.exc.HTTPNotFound, self.controller.show,
req, '777')
def test_view_builder_show(self):
view_builder = views_types.ViewBuilder()
now = timeutils.isotime()
raw_volume_type = dict(
name='new_type',
deleted=False,
created_at=now,
updated_at=now,
extra_specs={},
deleted_at=None,
id=42,
)
request = fakes.HTTPRequest.blank("/v2")
output = view_builder.show(request, raw_volume_type)
self.assertIn('volume_type', output)
expected_volume_type = dict(
name='new_type',
extra_specs={},
id=42,
)
self.assertDictMatch(output['volume_type'], expected_volume_type)
def test_view_builder_list(self):
view_builder = views_types.ViewBuilder()
now = timeutils.isotime()
raw_volume_types = []
for i in range(0, 10):
raw_volume_types.append(
dict(
name='new_type',
deleted=False,
created_at=now,
updated_at=now,
extra_specs={},
deleted_at=None,
id=42 + i
)
)
request = fakes.HTTPRequest.blank("/v2")
output = view_builder.index(request, raw_volume_types)
self.assertIn('volume_types', output)
for i in range(0, 10):
expected_volume_type = dict(
name='new_type',
extra_specs={},
id=42 + i
)
self.assertDictMatch(output['volume_types'][i],
expected_volume_type)

View File

@ -18,6 +18,8 @@
"share:update_share_metadata": [],
"share_extension:share_admin_actions:reset_status": [["rule:admin_api"]],
"share_extension:snapshot_admin_actions:reset_status": [["rule:admin_api"]],
"share_extension:types_manage": [],
"share_extension:types_extra_specs": [],
"limits_extension:used_limits": []
}

View File

@ -44,6 +44,7 @@ def fake_share(id, **kwargs):
'project_id': 'fakeproject',
'snapshot_id': None,
'share_network_id': None,
'volume_type_id': None,
'availability_zone': 'fakeaz',
'status': 'fakestatus',
'display_name': 'fakename',
@ -136,6 +137,7 @@ class ShareAPITestCase(test.TestCase):
'share_proto': share['share_proto'],
'share_id': share['id'],
'snapshot_id': share['snapshot_id'],
'volume_type': None
}
self.mox.StubOutWithMock(db_driver, 'share_create')
@ -243,6 +245,7 @@ class ShareAPITestCase(test.TestCase):
request_spec = {'share_properties': options,
'share_proto': share['share_proto'],
'share_id': share['id'],
'volume_type': None,
'snapshot_id': share['snapshot_id'],
}