From dd4cbb9096b2d7e7d7a1286aabc56ef738f35f75 Mon Sep 17 00:00:00 2001 From: Yulia Portnova Date: Thu, 17 Apr 2014 10:18:35 +0300 Subject: [PATCH] Added API to manage volume types Partially-implements bp volume-type-support Change-Id: I1a58cf82c659b49e5258a563788cb8e5b05bfb8a --- .../api/share/admin/test_volume_types.py | 100 ++++++ .../admin/test_volume_types_extra_specs.py | 145 +++++++++ .../test_volume_types_extra_specs_negative.py | 233 +++++++++++++ .../share/admin/test_volume_types_negative.py | 83 +++++ contrib/tempest/tempest/api/share/base.py | 27 +- .../services/share/json/shares_client.py | 81 ++++- etc/manila/policy.json | 3 + manila/api/common.py | 16 + manila/api/contrib/types_extra_specs.py | 172 ++++++++++ manila/api/contrib/types_manage.py | 124 +++++++ manila/api/v1/router.py | 7 + manila/api/v1/shares.py | 17 + manila/api/v1/volume_types.py | 51 +++ manila/api/views/shares.py | 5 + manila/api/views/types.py | 34 ++ manila/scheduler/filter_scheduler.py | 6 +- manila/share/api.py | 11 +- manila/share/volume_types.py | 181 +++++++++++ manila/tests/api/contrib/stubs.py | 1 + .../api/contrib/test_types_extra_specs.py | 307 ++++++++++++++++++ manila/tests/api/contrib/test_types_manage.py | 148 +++++++++ manila/tests/api/v1/test_shares.py | 2 + manila/tests/api/v1/test_volume_types.py | 168 ++++++++++ manila/tests/policy.json | 2 + manila/tests/test_share_api.py | 3 + 25 files changed, 1919 insertions(+), 8 deletions(-) create mode 100644 contrib/tempest/tempest/api/share/admin/test_volume_types.py create mode 100644 contrib/tempest/tempest/api/share/admin/test_volume_types_extra_specs.py create mode 100644 contrib/tempest/tempest/api/share/admin/test_volume_types_extra_specs_negative.py create mode 100644 contrib/tempest/tempest/api/share/admin/test_volume_types_negative.py create mode 100644 manila/api/contrib/types_extra_specs.py create mode 100644 manila/api/contrib/types_manage.py create mode 100644 manila/api/v1/volume_types.py create mode 100644 manila/api/views/types.py create mode 100644 manila/share/volume_types.py create mode 100644 manila/tests/api/contrib/test_types_extra_specs.py create mode 100644 manila/tests/api/contrib/test_types_manage.py create mode 100644 manila/tests/api/v1/test_volume_types.py diff --git a/contrib/tempest/tempest/api/share/admin/test_volume_types.py b/contrib/tempest/tempest/api/share/admin/test_volume_types.py new file mode 100644 index 0000000000..14e9a360e5 --- /dev/null +++ b/contrib/tempest/tempest/api/share/admin/test_volume_types.py @@ -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"]) diff --git a/contrib/tempest/tempest/api/share/admin/test_volume_types_extra_specs.py b/contrib/tempest/tempest/api/share/admin/test_volume_types_extra_specs.py new file mode 100644 index 0000000000..a53191f93f --- /dev/null +++ b/contrib/tempest/tempest/api/share/admin/test_volume_types_extra_specs.py @@ -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) diff --git a/contrib/tempest/tempest/api/share/admin/test_volume_types_extra_specs_negative.py b/contrib/tempest/tempest/api/share/admin/test_volume_types_extra_specs_negative.py new file mode 100644 index 0000000000..f422a9bcb6 --- /dev/null +++ b/contrib/tempest/tempest/api/share/admin/test_volume_types_extra_specs_negative.py @@ -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"}) diff --git a/contrib/tempest/tempest/api/share/admin/test_volume_types_negative.py b/contrib/tempest/tempest/api/share/admin/test_volume_types_negative.py new file mode 100644 index 0000000000..8d1384b02b --- /dev/null +++ b/contrib/tempest/tempest/api/share/admin/test_volume_types_negative.py @@ -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"]) diff --git a/contrib/tempest/tempest/api/share/base.py b/contrib/tempest/tempest/api/share/base.py index 98f4585769..59a428adf9 100644 --- a/contrib/tempest/tempest/api/share/base.py +++ b/contrib/tempest/tempest/api/share/base.py @@ -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: diff --git a/contrib/tempest/tempest/services/share/json/shares_client.py b/contrib/tempest/tempest/services/share/json/shares_client.py index c2e0a1c8c6..ac9129797d 100644 --- a/contrib/tempest/tempest/services/share/json/shares_client.py +++ b/contrib/tempest/tempest/services/share/json/shares_client.py @@ -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) diff --git a/etc/manila/policy.json b/etc/manila/policy.json index 56a7696d0b..e5a78c34bb 100644 --- a/etc/manila/policy.json +++ b/etc/manila/policy.json @@ -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": [], diff --git a/manila/api/common.py b/manila/api/common.py index 6897d8cb70..bcbee0d636 100644 --- a/manila/api/common.py +++ b/manila/api/common.py @@ -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. diff --git a/manila/api/contrib/types_extra_specs.py b/manila/api/contrib/types_extra_specs.py new file mode 100644 index 0000000000..cc7db28a39 --- /dev/null +++ b/manila/api/contrib/types_extra_specs.py @@ -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 diff --git a/manila/api/contrib/types_manage.py b/manila/api/contrib/types_manage.py new file mode 100644 index 0000000000..338d59975f --- /dev/null +++ b/manila/api/contrib/types_manage.py @@ -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] diff --git a/manila/api/v1/router.py b/manila/api/v1/router.py index e4c170470e..b773c6d905 100644 --- a/manila/api/v1/router.py +++ b/manila/api/v1/router.py @@ -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'}) diff --git a/manila/api/v1/shares.py b/manila/api/v1/shares.py index c5e03d1361..1e71d2cfa4 100644 --- a/manila/api/v1/shares.py +++ b/manila/api/v1/shares.py @@ -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, diff --git a/manila/api/v1/volume_types.py b/manila/api/v1/volume_types.py new file mode 100644 index 0000000000..3944b0ae9a --- /dev/null +++ b/manila/api/v1/volume_types.py @@ -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()) diff --git a/manila/api/views/shares.py b/manila/api/views/shares.py index 618fc047e8..810915deb0 100644 --- a/manila/api/views/shares.py +++ b/manila/api/views/shares.py @@ -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']) } } diff --git a/manila/api/views/types.py b/manila/api/views/types.py new file mode 100644 index 0000000000..b160784f21 --- /dev/null +++ b/manila/api/views/types.py @@ -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) diff --git a/manila/scheduler/filter_scheduler.py b/manila/scheduler/filter_scheduler.py index 64f9a3ee6c..49af893fb3 100644 --- a/manila/scheduler/filter_scheduler.py +++ b/manila/scheduler/filter_scheduler.py @@ -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 }) diff --git a/manila/share/api.py b/manila/share/api.py index 1f471cb315..68ca1a79c5 100644 --- a/manila/share/api.py +++ b/manila/share/api.py @@ -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 = {} diff --git a/manila/share/volume_types.py b/manila/share/volume_types.py new file mode 100644 index 0000000000..efca06efa5 --- /dev/null +++ b/manila/share/volume_types.py @@ -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) diff --git a/manila/tests/api/contrib/stubs.py b/manila/tests/api/contrib/stubs.py index d155ac4ce3..dc4a7d9f2b 100644 --- a/manila/tests/api/contrib/stubs.py +++ b/manila/tests/api/contrib/stubs.py @@ -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) diff --git a/manila/tests/api/contrib/test_types_extra_specs.py b/manila/tests/api/contrib/test_types_extra_specs.py new file mode 100644 index 0000000000..3292ecb11a --- /dev/null +++ b/manila/tests/api/contrib/test_types_extra_specs.py @@ -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) diff --git a/manila/tests/api/contrib/test_types_manage.py b/manila/tests/api/contrib/test_types_manage.py new file mode 100644 index 0000000000..8a69c55244 --- /dev/null +++ b/manila/tests/api/contrib/test_types_manage.py @@ -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'}) diff --git a/manila/tests/api/v1/test_shares.py b/manila/tests/api/v1/test_shares.py index 4d082e2450..2b7227ee62 100644 --- a/manila/tests/api/v1/test_shares.py +++ b/manila/tests/api/v1/test_shares.py @@ -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', diff --git a/manila/tests/api/v1/test_volume_types.py b/manila/tests/api/v1/test_volume_types.py new file mode 100644 index 0000000000..963f663cc0 --- /dev/null +++ b/manila/tests/api/v1/test_volume_types.py @@ -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) diff --git a/manila/tests/policy.json b/manila/tests/policy.json index e560c7bbf7..df0d780e63 100644 --- a/manila/tests/policy.json +++ b/manila/tests/policy.json @@ -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": [] } diff --git a/manila/tests/test_share_api.py b/manila/tests/test_share_api.py index f5676bc1f0..9e315b2b7a 100644 --- a/manila/tests/test_share_api.py +++ b/manila/tests/test_share_api.py @@ -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'], }