Merge "port some flavor tests into nova v3 part1"
diff --git a/tempest/api/compute/v3/admin/test_flavors_access.py b/tempest/api/compute/v3/admin/test_flavors_access.py
new file mode 100644
index 0000000..b866db1
--- /dev/null
+++ b/tempest/api/compute/v3/admin/test_flavors_access.py
@@ -0,0 +1,112 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 NEC Corporation.
+# 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 import compute
+from tempest.api.compute import base
+from tempest.common.utils import data_utils
+from tempest.test import attr
+
+
+class FlavorsAccessTestJSON(base.BaseV2ComputeAdminTest):
+
+ """
+ Tests Flavor Access API extension.
+ Add and remove Flavor Access require admin privileges.
+ """
+
+ _interface = 'json'
+
+ @classmethod
+ def setUpClass(cls):
+ super(FlavorsAccessTestJSON, cls).setUpClass()
+ if not compute.FLAVOR_EXTRA_DATA_ENABLED:
+ msg = "FlavorExtraData extension not enabled."
+ raise cls.skipException(msg)
+
+ cls.client = cls.os_adm.flavors_client
+ admin_client = cls._get_identity_admin_client()
+ cls.tenant = admin_client.get_tenant_by_name(cls.flavors_client.
+ tenant_name)
+ cls.tenant_id = cls.tenant['id']
+ cls.adm_tenant = admin_client.get_tenant_by_name(cls.os_adm.
+ flavors_client.
+ tenant_name)
+ cls.adm_tenant_id = cls.adm_tenant['id']
+ cls.flavor_name_prefix = 'test_flavor_access_'
+ cls.ram = 512
+ cls.vcpus = 1
+ cls.disk = 10
+
+ @attr(type='gate')
+ def test_flavor_access_list_with_private_flavor(self):
+ # Test to list flavor access successfully by querying private flavor
+ flavor_name = data_utils.rand_name(self.flavor_name_prefix)
+ new_flavor_id = data_utils.rand_int_id(start=1000)
+ resp, new_flavor = self.client.create_flavor(flavor_name,
+ self.ram, self.vcpus,
+ self.disk,
+ new_flavor_id,
+ is_public='False')
+ self.addCleanup(self.client.delete_flavor, new_flavor['id'])
+ self.assertEqual(resp.status, 200)
+ resp, flavor_access = self.client.list_flavor_access(new_flavor_id)
+ self.assertEqual(resp.status, 200)
+ self.assertEqual(len(flavor_access), 1, str(flavor_access))
+ first_flavor = flavor_access[0]
+ self.assertEqual(str(new_flavor_id), str(first_flavor['flavor_id']))
+ self.assertEqual(self.adm_tenant_id, first_flavor['tenant_id'])
+
+ @attr(type='gate')
+ def test_flavor_access_add_remove(self):
+ # Test to add and remove flavor access to a given tenant.
+ flavor_name = data_utils.rand_name(self.flavor_name_prefix)
+ new_flavor_id = data_utils.rand_int_id(start=1000)
+ resp, new_flavor = self.client.create_flavor(flavor_name,
+ self.ram, self.vcpus,
+ self.disk,
+ new_flavor_id,
+ is_public='False')
+ self.addCleanup(self.client.delete_flavor, new_flavor['id'])
+ # Add flavor access to a tenant.
+ resp_body = {
+ "tenant_id": str(self.tenant_id),
+ "flavor_id": str(new_flavor['id']),
+ }
+ add_resp, add_body = \
+ self.client.add_flavor_access(new_flavor['id'], self.tenant_id)
+ self.assertEqual(add_resp.status, 200)
+ self.assertIn(resp_body, add_body)
+
+ # The flavor is present in list.
+ resp, flavors = self.flavors_client.list_flavors_with_detail()
+ self.assertEqual(resp.status, 200)
+ self.assertIn(new_flavor['id'], map(lambda x: x['id'], flavors))
+
+ # Remove flavor access from a tenant.
+ remove_resp, remove_body = \
+ self.client.remove_flavor_access(new_flavor['id'], self.tenant_id)
+ self.assertEqual(remove_resp.status, 200)
+ self.assertNotIn(resp_body, remove_body)
+
+ # The flavor is not present in list.
+ resp, flavors = self.flavors_client.list_flavors_with_detail()
+ self.assertEqual(resp.status, 200)
+ self.assertNotIn(new_flavor['id'], map(lambda x: x['id'], flavors))
+
+
+class FlavorsAdminTestXML(FlavorsAccessTestJSON):
+ _interface = 'xml'
diff --git a/tempest/api/compute/v3/admin/test_flavors_access_negative.py b/tempest/api/compute/v3/admin/test_flavors_access_negative.py
new file mode 100644
index 0000000..340c1c7
--- /dev/null
+++ b/tempest/api/compute/v3/admin/test_flavors_access_negative.py
@@ -0,0 +1,153 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 IBM Corporation
+# 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 uuid
+
+from tempest.api import compute
+from tempest.api.compute import base
+from tempest.common.utils import data_utils
+from tempest import exceptions
+from tempest.test import attr
+
+
+class FlavorsAccessNegativeTestJSON(base.BaseV2ComputeAdminTest):
+
+ """
+ Tests Flavor Access API extension.
+ Add and remove Flavor Access require admin privileges.
+ """
+
+ _interface = 'json'
+
+ @classmethod
+ def setUpClass(cls):
+ super(FlavorsAccessNegativeTestJSON, cls).setUpClass()
+ if not compute.FLAVOR_EXTRA_DATA_ENABLED:
+ msg = "FlavorExtraData extension not enabled."
+ raise cls.skipException(msg)
+
+ cls.client = cls.os_adm.flavors_client
+ admin_client = cls._get_identity_admin_client()
+ cls.tenant = admin_client.get_tenant_by_name(cls.flavors_client.
+ tenant_name)
+ cls.tenant_id = cls.tenant['id']
+ cls.adm_tenant = admin_client.get_tenant_by_name(cls.os_adm.
+ flavors_client.
+ tenant_name)
+ cls.adm_tenant_id = cls.adm_tenant['id']
+ cls.flavor_name_prefix = 'test_flavor_access_'
+ cls.ram = 512
+ cls.vcpus = 1
+ cls.disk = 10
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_access_list_with_public_flavor(self):
+ # Test to list flavor access with exceptions by querying public flavor
+ flavor_name = data_utils.rand_name(self.flavor_name_prefix)
+ new_flavor_id = data_utils.rand_int_id(start=1000)
+ resp, new_flavor = self.client.create_flavor(flavor_name,
+ self.ram, self.vcpus,
+ self.disk,
+ new_flavor_id,
+ is_public='True')
+ self.addCleanup(self.client.delete_flavor, new_flavor['id'])
+ self.assertEqual(resp.status, 200)
+ self.assertRaises(exceptions.NotFound,
+ self.client.list_flavor_access,
+ new_flavor_id)
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_non_admin_add(self):
+ # Test to add flavor access as a user without admin privileges.
+ flavor_name = data_utils.rand_name(self.flavor_name_prefix)
+ new_flavor_id = data_utils.rand_int_id(start=1000)
+ resp, new_flavor = self.client.create_flavor(flavor_name,
+ self.ram, self.vcpus,
+ self.disk,
+ new_flavor_id,
+ is_public='False')
+ self.addCleanup(self.client.delete_flavor, new_flavor['id'])
+ self.assertRaises(exceptions.Unauthorized,
+ self.flavors_client.add_flavor_access,
+ new_flavor['id'],
+ self.tenant_id)
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_non_admin_remove(self):
+ # Test to remove flavor access as a user without admin privileges.
+ flavor_name = data_utils.rand_name(self.flavor_name_prefix)
+ new_flavor_id = data_utils.rand_int_id(start=1000)
+ resp, new_flavor = self.client.create_flavor(flavor_name,
+ self.ram, self.vcpus,
+ self.disk,
+ new_flavor_id,
+ is_public='False')
+ self.addCleanup(self.client.delete_flavor, new_flavor['id'])
+ # Add flavor access to a tenant.
+ self.client.add_flavor_access(new_flavor['id'], self.tenant_id)
+ self.addCleanup(self.client.remove_flavor_access,
+ new_flavor['id'], self.tenant_id)
+ self.assertRaises(exceptions.Unauthorized,
+ self.flavors_client.remove_flavor_access,
+ new_flavor['id'],
+ self.tenant_id)
+
+ @attr(type=['negative', 'gate'])
+ def test_add_flavor_access_duplicate(self):
+ # Create a new flavor.
+ flavor_name = data_utils.rand_name(self.flavor_name_prefix)
+ new_flavor_id = data_utils.rand_int_id(start=1000)
+ resp, new_flavor = self.client.create_flavor(flavor_name,
+ self.ram, self.vcpus,
+ self.disk,
+ new_flavor_id,
+ is_public='False')
+ self.addCleanup(self.client.delete_flavor, new_flavor['id'])
+
+ # Add flavor access to a tenant.
+ self.client.add_flavor_access(new_flavor['id'], self.tenant_id)
+ self.addCleanup(self.client.remove_flavor_access,
+ new_flavor['id'], self.tenant_id)
+
+ # An exception should be raised when adding flavor access to the same
+ # tenant
+ self.assertRaises(exceptions.Conflict,
+ self.client.add_flavor_access,
+ new_flavor['id'],
+ self.tenant_id)
+
+ @attr(type=['negative', 'gate'])
+ def test_remove_flavor_access_not_found(self):
+ # Create a new flavor.
+ flavor_name = data_utils.rand_name(self.flavor_name_prefix)
+ new_flavor_id = data_utils.rand_int_id(start=1000)
+ resp, new_flavor = self.client.create_flavor(flavor_name,
+ self.ram, self.vcpus,
+ self.disk,
+ new_flavor_id,
+ is_public='False')
+ self.addCleanup(self.client.delete_flavor, new_flavor['id'])
+
+ # An exception should be raised when flavor access is not found
+ self.assertRaises(exceptions.NotFound,
+ self.client.remove_flavor_access,
+ new_flavor['id'],
+ str(uuid.uuid4()))
+
+
+class FlavorsAdminNegativeTestXML(FlavorsAccessNegativeTestJSON):
+ _interface = 'xml'
diff --git a/tempest/api/compute/v3/admin/test_flavors_extra_specs.py b/tempest/api/compute/v3/admin/test_flavors_extra_specs.py
new file mode 100644
index 0000000..49b0429
--- /dev/null
+++ b/tempest/api/compute/v3/admin/test_flavors_extra_specs.py
@@ -0,0 +1,132 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 tempest.api import compute
+from tempest.api.compute import base
+from tempest.common.utils import data_utils
+from tempest.test import attr
+
+
+class FlavorsExtraSpecsTestJSON(base.BaseV2ComputeAdminTest):
+
+ """
+ Tests Flavor Extra Spec API extension.
+ SET, UNSET, UPDATE Flavor Extra specs require admin privileges.
+ GET Flavor Extra specs can be performed even by without admin privileges.
+ """
+
+ _interface = 'json'
+
+ @classmethod
+ def setUpClass(cls):
+ super(FlavorsExtraSpecsTestJSON, cls).setUpClass()
+ if not compute.FLAVOR_EXTRA_DATA_ENABLED:
+ msg = "FlavorExtraData extension not enabled."
+ raise cls.skipException(msg)
+
+ cls.client = cls.os_adm.flavors_client
+ flavor_name = data_utils.rand_name('test_flavor')
+ ram = 512
+ vcpus = 1
+ disk = 10
+ ephemeral = 10
+ cls.new_flavor_id = data_utils.rand_int_id(start=1000)
+ swap = 1024
+ rxtx = 1
+ # Create a flavor so as to set/get/unset extra specs
+ resp, cls.flavor = cls.client.create_flavor(flavor_name,
+ ram, vcpus,
+ disk,
+ cls.new_flavor_id,
+ ephemeral=ephemeral,
+ swap=swap, rxtx=rxtx)
+
+ @classmethod
+ def tearDownClass(cls):
+ resp, body = cls.client.delete_flavor(cls.flavor['id'])
+ cls.client.wait_for_resource_deletion(cls.flavor['id'])
+ super(FlavorsExtraSpecsTestJSON, cls).tearDownClass()
+
+ @attr(type='gate')
+ def test_flavor_set_get_update_show_unset_keys(self):
+ # Test to SET, GET, UPDATE, SHOW, UNSET flavor extra
+ # spec as a user with admin privileges.
+ # Assigning extra specs values that are to be set
+ specs = {"key1": "value1", "key2": "value2"}
+ # SET extra specs to the flavor created in setUp
+ set_resp, set_body = \
+ self.client.set_flavor_extra_spec(self.flavor['id'], specs)
+ self.assertEqual(set_resp.status, 200)
+ self.assertEqual(set_body, specs)
+ # GET extra specs and verify
+ get_resp, get_body = \
+ self.client.get_flavor_extra_spec(self.flavor['id'])
+ self.assertEqual(get_resp.status, 200)
+ self.assertEqual(get_body, specs)
+
+ # UPDATE the value of the extra specs key1
+ update_resp, update_body = \
+ self.client.update_flavor_extra_spec(self.flavor['id'],
+ "key1",
+ key1="value")
+ self.assertEqual(update_resp.status, 200)
+ self.assertEqual({"key1": "value"}, update_body)
+
+ # GET extra specs and verify the value of the key2
+ # is the same as before
+ get_resp, get_body = \
+ self.client.get_flavor_extra_spec(self.flavor['id'])
+ self.assertEqual(get_resp.status, 200)
+ self.assertEqual(get_body, {"key1": "value", "key2": "value2"})
+
+ # UNSET extra specs that were set in this test
+ unset_resp, _ = \
+ self.client.unset_flavor_extra_spec(self.flavor['id'], "key1")
+ self.assertEqual(unset_resp.status, 200)
+ unset_resp, _ = \
+ self.client.unset_flavor_extra_spec(self.flavor['id'], "key2")
+ self.assertEqual(unset_resp.status, 200)
+
+ @attr(type='gate')
+ def test_flavor_non_admin_get_all_keys(self):
+ specs = {"key1": "value1", "key2": "value2"}
+ set_resp, set_body = self.client.set_flavor_extra_spec(
+ self.flavor['id'], specs)
+ resp, body = self.flavors_client.get_flavor_extra_spec(
+ self.flavor['id'])
+ self.assertEqual(resp.status, 200)
+
+ for key in specs:
+ self.assertEqual(body[key], specs[key])
+
+ @attr(type='gate')
+ def test_flavor_non_admin_get_specific_key(self):
+ specs = {"key1": "value1", "key2": "value2"}
+ resp, body = self.client.set_flavor_extra_spec(
+ self.flavor['id'], specs)
+ self.assertEqual(resp.status, 200)
+ self.assertEqual(body['key1'], 'value1')
+ self.assertIn('key2', body)
+ resp, body = self.flavors_client.get_flavor_extra_spec_with_key(
+ self.flavor['id'], 'key1')
+ self.assertEqual(resp.status, 200)
+ self.assertEqual(body['key1'], 'value1')
+ self.assertNotIn('key2', body)
+
+
+class FlavorsExtraSpecsTestXML(FlavorsExtraSpecsTestJSON):
+ _interface = 'xml'
diff --git a/tempest/api/compute/v3/admin/test_flavors_extra_specs_negative.py b/tempest/api/compute/v3/admin/test_flavors_extra_specs_negative.py
new file mode 100644
index 0000000..d7e1f9f
--- /dev/null
+++ b/tempest/api/compute/v3/admin/test_flavors_extra_specs_negative.py
@@ -0,0 +1,136 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack Foundation
+# All Rights Reserved.
+# Copyright 2013 IBM Corp.
+#
+# 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 import compute
+from tempest.api.compute import base
+from tempest.common.utils import data_utils
+from tempest import exceptions
+from tempest.test import attr
+
+
+class FlavorsExtraSpecsNegativeTestJSON(base.BaseV2ComputeAdminTest):
+
+ """
+ Negative Tests Flavor Extra Spec API extension.
+ SET, UNSET, UPDATE Flavor Extra specs require admin privileges.
+ """
+
+ _interface = 'json'
+
+ @classmethod
+ def setUpClass(cls):
+ super(FlavorsExtraSpecsNegativeTestJSON, cls).setUpClass()
+ if not compute.FLAVOR_EXTRA_DATA_ENABLED:
+ msg = "FlavorExtraData extension not enabled."
+ raise cls.skipException(msg)
+
+ cls.client = cls.os_adm.flavors_client
+ flavor_name = data_utils.rand_name('test_flavor')
+ ram = 512
+ vcpus = 1
+ disk = 10
+ ephemeral = 10
+ cls.new_flavor_id = data_utils.rand_int_id(start=1000)
+ swap = 1024
+ rxtx = 1
+ # Create a flavor
+ resp, cls.flavor = cls.client.create_flavor(flavor_name,
+ ram, vcpus,
+ disk,
+ cls.new_flavor_id,
+ ephemeral=ephemeral,
+ swap=swap, rxtx=rxtx)
+
+ @classmethod
+ def tearDownClass(cls):
+ resp, body = cls.client.delete_flavor(cls.flavor['id'])
+ cls.client.wait_for_resource_deletion(cls.flavor['id'])
+ super(FlavorsExtraSpecsNegativeTestJSON, cls).tearDownClass()
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_non_admin_set_keys(self):
+ # Test to SET flavor extra spec as a user without admin privileges.
+ specs = {"key1": "value1", "key2": "value2"}
+ self.assertRaises(exceptions.Unauthorized,
+ self.flavors_client.set_flavor_extra_spec,
+ self.flavor['id'],
+ specs)
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_non_admin_update_specific_key(self):
+ # non admin user is not allowed to update flavor extra spec
+ specs = {"key1": "value1", "key2": "value2"}
+ resp, body = self.client.set_flavor_extra_spec(
+ self.flavor['id'], specs)
+ self.assertEqual(resp.status, 200)
+ self.assertEqual(body['key1'], 'value1')
+ self.assertRaises(exceptions.Unauthorized,
+ self.flavors_client.
+ update_flavor_extra_spec,
+ self.flavor['id'],
+ 'key1',
+ key1='value1_new')
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_non_admin_unset_keys(self):
+ specs = {"key1": "value1", "key2": "value2"}
+ set_resp, set_body = self.client.set_flavor_extra_spec(
+ self.flavor['id'], specs)
+
+ self.assertRaises(exceptions.Unauthorized,
+ self.flavors_client.unset_flavor_extra_spec,
+ self.flavor['id'],
+ 'key1')
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_unset_nonexistent_key(self):
+ nonexistent_key = data_utils.rand_name('flavor_key')
+ self.assertRaises(exceptions.NotFound,
+ self.client.unset_flavor_extra_spec,
+ self.flavor['id'],
+ nonexistent_key)
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_get_nonexistent_key(self):
+ self.assertRaises(exceptions.NotFound,
+ self.flavors_client.get_flavor_extra_spec_with_key,
+ self.flavor['id'],
+ "nonexistent_key")
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_update_mismatch_key(self):
+ # the key will be updated should be match the key in the body
+ self.assertRaises(exceptions.BadRequest,
+ self.client.update_flavor_extra_spec,
+ self.flavor['id'],
+ "key2",
+ key1="value")
+
+ @attr(type=['negative', 'gate'])
+ def test_flavor_update_more_key(self):
+ # there should be just one item in the request body
+ self.assertRaises(exceptions.BadRequest,
+ self.client.update_flavor_extra_spec,
+ self.flavor['id'],
+ "key1",
+ key1="value",
+ key2="value")
+
+
+class FlavorsExtraSpecsNegativeTestXML(FlavorsExtraSpecsNegativeTestJSON):
+ _interface = 'xml'
diff --git a/tempest/services/compute/v3/json/flavors_client.py b/tempest/services/compute/v3/json/flavors_client.py
new file mode 100644
index 0000000..00d6f8a
--- /dev/null
+++ b/tempest/services/compute/v3/json/flavors_client.py
@@ -0,0 +1,156 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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.
+
+import json
+import urllib
+
+from tempest.common.rest_client import RestClient
+
+
+class FlavorsClientJSON(RestClient):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(FlavorsClientJSON, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
+
+ def list_flavors(self, params=None):
+ url = 'flavors'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ return resp, body['flavors']
+
+ def list_flavors_with_detail(self, params=None):
+ url = 'flavors/detail'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ return resp, body['flavors']
+
+ def get_flavor_details(self, flavor_id):
+ resp, body = self.get("flavors/%s" % str(flavor_id))
+ body = json.loads(body)
+ return resp, body['flavor']
+
+ def create_flavor(self, name, ram, vcpus, disk, flavor_id, **kwargs):
+ """Creates a new flavor or instance type."""
+ post_body = {
+ 'name': name,
+ 'ram': ram,
+ 'vcpus': vcpus,
+ 'disk': disk,
+ 'id': flavor_id,
+ }
+ if kwargs.get('ephemeral'):
+ post_body['OS-FLV-EXT-DATA:ephemeral'] = kwargs.get('ephemeral')
+ if kwargs.get('swap'):
+ post_body['swap'] = kwargs.get('swap')
+ if kwargs.get('rxtx'):
+ post_body['rxtx_factor'] = kwargs.get('rxtx')
+ if kwargs.get('is_public'):
+ post_body['os-flavor-access:is_public'] = kwargs.get('is_public')
+ post_body = json.dumps({'flavor': post_body})
+ resp, body = self.post('flavors', post_body, self.headers)
+
+ body = json.loads(body)
+ return resp, body['flavor']
+
+ def delete_flavor(self, flavor_id):
+ """Deletes the given flavor."""
+ return self.delete("flavors/%s" % str(flavor_id))
+
+ def is_resource_deleted(self, id):
+ # Did not use get_flavor_details(id) for verification as it gives
+ # 200 ok even for deleted id. LP #981263
+ # we can remove the loop here and use get by ID when bug gets sortedout
+ resp, flavors = self.list_flavors_with_detail()
+ for flavor in flavors:
+ if flavor['id'] == id:
+ return False
+ return True
+
+ def set_flavor_extra_spec(self, flavor_id, specs):
+ """Sets extra Specs to the mentioned flavor."""
+ post_body = json.dumps({'extra_specs': specs})
+ resp, body = self.post('flavors/%s/os-extra_specs' % flavor_id,
+ post_body, self.headers)
+ body = json.loads(body)
+ return resp, body['extra_specs']
+
+ def get_flavor_extra_spec(self, flavor_id):
+ """Gets extra Specs details of the mentioned flavor."""
+ resp, body = self.get('flavors/%s/os-extra_specs' % flavor_id)
+ body = json.loads(body)
+ return resp, body['extra_specs']
+
+ def get_flavor_extra_spec_with_key(self, flavor_id, key):
+ """Gets extra Specs key-value of the mentioned flavor and key."""
+ resp, body = self.get('flavors/%s/os-extra_specs/%s' % (str(flavor_id),
+ key))
+ body = json.loads(body)
+ return resp, body
+
+ def update_flavor_extra_spec(self, flavor_id, key, **kwargs):
+ """Update specified extra Specs of the mentioned flavor and key."""
+ resp, body = self.put('flavors/%s/os-extra_specs/%s' %
+ (flavor_id, key),
+ json.dumps(kwargs), self.headers)
+ body = json.loads(body)
+ return resp, body
+
+ def unset_flavor_extra_spec(self, flavor_id, key):
+ """Unsets extra Specs from the mentioned flavor."""
+ return self.delete('flavors/%s/os-extra_specs/%s' % (str(flavor_id),
+ key))
+
+ def list_flavor_access(self, flavor_id):
+ """Gets flavor access information given the flavor id."""
+ resp, body = self.get('flavors/%s/os-flavor-access' % flavor_id,
+ self.headers)
+ body = json.loads(body)
+ return resp, body['flavor_access']
+
+ def add_flavor_access(self, flavor_id, tenant_id):
+ """Add flavor access for the specified tenant."""
+ post_body = {
+ 'addTenantAccess': {
+ 'tenant': tenant_id
+ }
+ }
+ post_body = json.dumps(post_body)
+ resp, body = self.post('flavors/%s/action' % flavor_id,
+ post_body, self.headers)
+ body = json.loads(body)
+ return resp, body['flavor_access']
+
+ def remove_flavor_access(self, flavor_id, tenant_id):
+ """Remove flavor access from the specified tenant."""
+ post_body = {
+ 'removeTenantAccess': {
+ 'tenant': tenant_id
+ }
+ }
+ post_body = json.dumps(post_body)
+ resp, body = self.post('flavors/%s/action' % flavor_id,
+ post_body, self.headers)
+ body = json.loads(body)
+ return resp, body['flavor_access']
diff --git a/tempest/services/compute/v3/xml/flavors_client.py b/tempest/services/compute/v3/xml/flavors_client.py
new file mode 100644
index 0000000..a1c74d9
--- /dev/null
+++ b/tempest/services/compute/v3/xml/flavors_client.py
@@ -0,0 +1,220 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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.
+
+import urllib
+
+from lxml import etree
+
+from tempest.common.rest_client import RestClientXML
+from tempest.services.compute.xml.common import Document
+from tempest.services.compute.xml.common import Element
+from tempest.services.compute.xml.common import Text
+from tempest.services.compute.xml.common import xml_to_json
+from tempest.services.compute.xml.common import XMLNS_11
+
+
+XMLNS_OS_FLV_EXT_DATA = \
+ "http://docs.openstack.org/compute/ext/flavor_extra_data/api/v1.1"
+XMLNS_OS_FLV_ACCESS = \
+ "http://docs.openstack.org/compute/ext/flavor_access/api/v2"
+
+
+class FlavorsClientXML(RestClientXML):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(FlavorsClientXML, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
+
+ def _format_flavor(self, f):
+ flavor = {'links': []}
+ for k, v in f.items():
+ if k == 'id':
+ flavor['id'] = v
+ continue
+
+ if k == 'link':
+ flavor['links'].append(v)
+ continue
+
+ if k == '{%s}ephemeral' % XMLNS_OS_FLV_EXT_DATA:
+ k = 'OS-FLV-EXT-DATA:ephemeral'
+
+ if k == '{%s}is_public' % XMLNS_OS_FLV_ACCESS:
+ k = 'os-flavor-access:is_public'
+ v = True if v == 'True' else False
+
+ if k == 'extra_specs':
+ k = 'OS-FLV-WITH-EXT-SPECS:extra_specs'
+ flavor[k] = dict(v)
+ continue
+
+ try:
+ v = int(v)
+ except ValueError:
+ try:
+ v = float(v)
+ except ValueError:
+ pass
+
+ flavor[k] = v
+
+ return flavor
+
+ def _parse_array(self, node):
+ return [self._format_flavor(xml_to_json(x)) for x in node]
+
+ def _list_flavors(self, url, params):
+ if params:
+ url += "?%s" % urllib.urlencode(params)
+
+ resp, body = self.get(url, self.headers)
+ flavors = self._parse_array(etree.fromstring(body))
+ return resp, flavors
+
+ def list_flavors(self, params=None):
+ url = 'flavors'
+ return self._list_flavors(url, params)
+
+ def list_flavors_with_detail(self, params=None):
+ url = 'flavors/detail'
+ return self._list_flavors(url, params)
+
+ def get_flavor_details(self, flavor_id):
+ resp, body = self.get("flavors/%s" % str(flavor_id), self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ flavor = self._format_flavor(body)
+ return resp, flavor
+
+ def create_flavor(self, name, ram, vcpus, disk, flavor_id, **kwargs):
+ """Creates a new flavor or instance type."""
+ flavor = Element("flavor",
+ xmlns=XMLNS_11,
+ ram=ram,
+ vcpus=vcpus,
+ disk=disk,
+ id=flavor_id,
+ name=name)
+ if kwargs.get('rxtx'):
+ flavor.add_attr('rxtx_factor', kwargs.get('rxtx'))
+ if kwargs.get('swap'):
+ flavor.add_attr('swap', kwargs.get('swap'))
+ if kwargs.get('ephemeral'):
+ flavor.add_attr('OS-FLV-EXT-DATA:ephemeral',
+ kwargs.get('ephemeral'))
+ if kwargs.get('is_public'):
+ flavor.add_attr('os-flavor-access:is_public',
+ kwargs.get('is_public'))
+ flavor.add_attr('xmlns:OS-FLV-EXT-DATA', XMLNS_OS_FLV_EXT_DATA)
+ flavor.add_attr('xmlns:os-flavor-access', XMLNS_OS_FLV_ACCESS)
+ resp, body = self.post('flavors', str(Document(flavor)), self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ flavor = self._format_flavor(body)
+ return resp, flavor
+
+ def delete_flavor(self, flavor_id):
+ """Deletes the given flavor."""
+ return self.delete("flavors/%s" % str(flavor_id), self.headers)
+
+ def is_resource_deleted(self, id):
+ # Did not use get_flavor_details(id) for verification as it gives
+ # 200 ok even for deleted id. LP #981263
+ # we can remove the loop here and use get by ID when bug gets sortedout
+ resp, flavors = self.list_flavors_with_detail()
+ for flavor in flavors:
+ if flavor['id'] == id:
+ return False
+ return True
+
+ def set_flavor_extra_spec(self, flavor_id, specs):
+ """Sets extra Specs to the mentioned flavor."""
+ extra_specs = Element("extra_specs")
+ for key in specs.keys():
+ extra_specs.add_attr(key, specs[key])
+ resp, body = self.post('flavors/%s/os-extra_specs' % flavor_id,
+ str(Document(extra_specs)), self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def get_flavor_extra_spec(self, flavor_id):
+ """Gets extra Specs of the mentioned flavor."""
+ resp, body = self.get('flavors/%s/os-extra_specs' % flavor_id,
+ self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def get_flavor_extra_spec_with_key(self, flavor_id, key):
+ """Gets extra Specs key-value of the mentioned flavor and key."""
+ resp, xml_body = self.get('flavors/%s/os-extra_specs/%s' %
+ (str(flavor_id), key), self.headers)
+ body = {}
+ element = etree.fromstring(xml_body)
+ key = element.get('key')
+ body[key] = xml_to_json(element)
+ return resp, body
+
+ def update_flavor_extra_spec(self, flavor_id, key, **kwargs):
+ """Update extra Specs details of the mentioned flavor and key."""
+ doc = Document()
+ for (k, v) in kwargs.items():
+ element = Element(k)
+ doc.append(element)
+ value = Text(v)
+ element.append(value)
+
+ resp, body = self.put('flavors/%s/os-extra_specs/%s' %
+ (flavor_id, key),
+ str(doc), self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, {key: body}
+
+ def unset_flavor_extra_spec(self, flavor_id, key):
+ """Unsets an extra spec based on the mentioned flavor and key."""
+ return self.delete('flavors/%s/os-extra_specs/%s' % (str(flavor_id),
+ key))
+
+ def _parse_array_access(self, node):
+ return [xml_to_json(x) for x in node]
+
+ def list_flavor_access(self, flavor_id):
+ """Gets flavor access information given the flavor id."""
+ resp, body = self.get('flavors/%s/os-flavor-access' % str(flavor_id),
+ self.headers)
+ body = self._parse_array(etree.fromstring(body))
+ return resp, body
+
+ def add_flavor_access(self, flavor_id, tenant_id):
+ """Add flavor access for the specified tenant."""
+ doc = Document()
+ server = Element("addTenantAccess")
+ doc.append(server)
+ server.add_attr("tenant", tenant_id)
+ resp, body = self.post('flavors/%s/action' % str(flavor_id),
+ str(doc), self.headers)
+ body = self._parse_array_access(etree.fromstring(body))
+ return resp, body
+
+ def remove_flavor_access(self, flavor_id, tenant_id):
+ """Remove flavor access from the specified tenant."""
+ doc = Document()
+ server = Element("removeTenantAccess")
+ doc.append(server)
+ server.add_attr("tenant", tenant_id)
+ resp, body = self.post('flavors/%s/action' % str(flavor_id),
+ str(doc), self.headers)
+ body = self._parse_array_access(etree.fromstring(body))
+ return resp, body