Negative Cinder tests for Volume Types,extra specs
* Adds negative tests for volume types.
* Adds negative tests for volume type extra specs.
* Adds xml client for volume types and extra specs.
Fixes LP Bug #1090229
Change-Id: I97ad07ffff7d85b5901fb4c23b70fe4a8814ebcb
diff --git a/tempest/services/volume/xml/admin/__init__.py b/tempest/services/volume/xml/admin/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/volume/xml/admin/__init__.py
diff --git a/tempest/services/volume/xml/admin/volume_types_client.py b/tempest/services/volume/xml/admin/volume_types_client.py
new file mode 100644
index 0000000..3da1af0
--- /dev/null
+++ b/tempest/services/volume/xml/admin/volume_types_client.py
@@ -0,0 +1,195 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 IBM
+# 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 import exceptions
+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
+
+
+class VolumeTypesClientXML(RestClientXML):
+ """
+ Client class to send CRUD Volume Types API requests to a Cinder endpoint
+ """
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(VolumeTypesClientXML, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.volume.catalog_type
+ self.build_interval = self.config.compute.build_interval
+ self.build_timeout = self.config.compute.build_timeout
+
+ def _parse_volume_type(self, body):
+ vol_type = dict((attr, body.get(attr)) for attr in body.keys())
+
+ for child in body.getchildren():
+ tag = child.tag
+ if tag.startswith("{"):
+ ns, tag = tag.split("}", 1)
+ if tag == 'extra_specs':
+ vol_type['extra_specs'] = dict((meta.get('key'),
+ meta.text)
+ for meta in list(child))
+ else:
+ vol_type[tag] = xml_to_json(child)
+ return vol_type
+
+ def _parse_volume_type_extra_specs(self, body):
+ extra_spec = dict((attr, body.get(attr)) for attr in body.keys())
+
+ for child in body.getchildren():
+ tag = child.tag
+ if tag.startswith("{"):
+ ns, tag = tag.split("}", 1)
+ else:
+ extra_spec[tag] = xml_to_json(child)
+ return extra_spec
+
+ def list_volume_types(self, params=None):
+ """List all the volume_types created"""
+ url = 'types'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ volume_types = []
+ if body is not None:
+ volume_types += [self._parse_volume_type(vol)
+ for vol in list(body)]
+ return resp, volume_types
+
+ def get_volume_type(self, type_id):
+ """Returns the details of a single volume_type"""
+ url = "types/%s" % str(type_id)
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ return resp, self._parse_volume_type(body)
+
+ def create_volume_type(self, name, **kwargs):
+ """
+ Creates a new Volume_type.
+ name(Required): Name of volume_type.
+ Following optional keyword arguments are accepted:
+ extra_specs: A dictionary of values to be used as extra_specs.
+ """
+ vol_type = Element("volume_type", xmlns=XMLNS_11)
+ if name:
+ vol_type.add_attr('name', name)
+
+ extra_specs = kwargs.get('extra_specs')
+ if extra_specs:
+ _extra_specs = Element('extra_specs')
+ vol_type.append(_extra_specs)
+ for key, value in extra_specs.items():
+ spec = Element('extra_spec')
+ spec.add_attr('key', key)
+ spec.append(Text(value))
+ _extra_specs.append(spec)
+
+ resp, body = self.post('types', str(Document(vol_type)),
+ self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def delete_volume_type(self, type_id):
+ """Deletes the Specified Volume_type"""
+ return self.delete("types/%s" % str(type_id))
+
+ def list_volume_types_extra_specs(self, vol_type_id, params=None):
+ """List all the volume_types extra specs created"""
+ url = 'types/%s/extra_specs' % str(vol_type_id)
+
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ extra_specs = []
+ if body is not None:
+ extra_specs += [self._parse_volume_type_extra_specs(spec)
+ for spec in list(body)]
+ return resp, extra_specs
+
+ def get_volume_type_extra_specs(self, vol_type_id, extra_spec_name):
+ """Returns the details of a single volume_type extra spec"""
+ url = "types/%s/extra_specs/%s" % (str(vol_type_id),
+ str(extra_spec_name))
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ return resp, self._parse_volume_type_extra_specs(body)
+
+ def create_volume_type_extra_specs(self, vol_type_id, extra_spec):
+ """
+ Creates a new Volume_type extra spec.
+ vol_type_id: Id of volume_type.
+ extra_specs: A dictionary of values to be used as extra_specs.
+ """
+ url = "types/%s/extra_specs" % str(vol_type_id)
+ extra_specs = Element("extra_specs", xmlns=XMLNS_11)
+ if extra_spec:
+ for key, value in extra_spec.items():
+ spec = Element('extra_spec')
+ spec.add_attr('key', key)
+ spec.append(Text(value))
+ extra_specs.append(spec)
+
+ resp, body = self.post(url, str(Document(extra_specs)),
+ self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def delete_volume_type_extra_specs(self, vol_id, extra_spec_name):
+ """Deletes the Specified Volume_type extra spec"""
+ return self.delete("types/%s/extra_specs/%s" % ((str(vol_id)),
+ str(extra_spec_name)))
+
+ def update_volume_type_extra_specs(self, vol_type_id, extra_spec_name,
+ extra_spec):
+ """
+ Update a volume_type extra spec.
+ vol_type_id: Id of volume_type.
+ extra_spec_name: Name of the extra spec to be updated.
+ extra_spec: A dictionary of with key as extra_spec_name and the
+ updated value.
+ """
+ url = "types/%s/extra_specs/%s" % (str(vol_type_id),
+ str(extra_spec_name))
+ extra_specs = Element("extra_specs", xmlns=XMLNS_11)
+ for key, value in extra_spec.items():
+ spec = Element('extra_spec')
+ spec.add_attr('key', key)
+ spec.append(Text(value))
+ extra_specs.append(spec)
+ resp, body = self.put(url, str(Document(extra_specs)),
+ self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def is_resource_deleted(self, id):
+ try:
+ self.get_volume_type(id)
+ except exceptions.NotFound:
+ return True
+ return False
diff --git a/tempest/tests/volume/admin/base.py b/tempest/tests/volume/admin/base.py
new file mode 100644
index 0000000..420da42
--- /dev/null
+++ b/tempest/tests/volume/admin/base.py
@@ -0,0 +1,67 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import nose
+
+
+from tempest import config
+import tempest.services.volume.json.admin.volume_types_client \
+ as volume_types_json_client
+import tempest.services.volume.xml.admin.volume_types_client \
+ as volume_types_xml_client
+from tempest.tests.volume.base import BaseVolumeTest
+
+
+class BaseVolumeAdminTest(BaseVolumeTest):
+ """Base test case class for all Volume Admin API tests"""
+ @classmethod
+ def setUpClass(cls):
+ super(BaseVolumeAdminTest, cls).setUpClass()
+ cls.config = config.TempestConfig()
+ cls.adm_user = cls.config.compute_admin.username
+ cls.adm_pass = cls.config.compute_admin.password
+ cls.adm_tenant = cls.config.compute_admin.tenant_name
+ cls.auth_url = cls.config.identity.auth_url
+
+ if not cls.adm_user and cls.adm_pass and cls.adm_tenant:
+ msg = ("Missing Volume Admin API credentials "
+ "in configuration.")
+ raise nose.SkipTest(msg)
+
+ @classmethod
+ def tearDownClass(cls):
+ super(BaseVolumeAdminTest, cls).tearDownClass()
+
+
+class BaseVolumeAdminTestJSON(BaseVolumeAdminTest):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = "json"
+ super(BaseVolumeAdminTestJSON, cls).setUpClass()
+ cls.client = volume_types_json_client.\
+ VolumeTypesClientJSON(cls.config, cls.adm_user, cls.adm_pass,
+ cls.auth_url, cls.adm_tenant)
+
+
+class BaseVolumeAdminTestXML(BaseVolumeAdminTest):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = "xml"
+ super(BaseVolumeAdminTestXML, cls).setUpClass()
+ cls.client = volume_types_xml_client.\
+ VolumeTypesClientXML(cls.config, cls.adm_user, cls.adm_pass,
+ cls.auth_url, cls.adm_tenant)
diff --git a/tempest/tests/volume/admin/test_volume_types_extra_specs_negative.py b/tempest/tests/volume/admin/test_volume_types_extra_specs_negative.py
new file mode 100644
index 0000000..bfbfaae
--- /dev/null
+++ b/tempest/tests/volume/admin/test_volume_types_extra_specs_negative.py
@@ -0,0 +1,168 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import unittest
+import uuid
+
+from nose.plugins.attrib import attr
+from nose.tools import raises
+
+from tempest.common.utils.data_utils import rand_name
+from tempest import exceptions
+from tempest.tests.volume.admin.base import BaseVolumeAdminTestJSON
+from tempest.tests.volume.admin.base import BaseVolumeAdminTestXML
+
+
+class ExtraSpecsNegativeTestBase():
+
+ @staticmethod
+ def setUpClass(cls):
+ cls.client = cls.client
+ vol_type_name = rand_name('Volume-type-')
+ cls.extra_specs = {"spec1": "val1"}
+ resp, cls.volume_type = cls.client.create_volume_type(vol_type_name,
+ extra_specs=
+ cls.extra_specs)
+
+ @staticmethod
+ def tearDownClass(cls):
+ cls.client.delete_volume_type(cls.volume_type['id'])
+
+ @unittest.skip('Until bug 1090320 is fixed')
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_update_no_body(self):
+ """ Should not update volume type extra specs with no body"""
+ extra_spec = {"spec1": "val2"}
+ self.client.update_volume_type_extra_specs(self.volume_type['id'],
+ extra_spec.keys()[0],
+ None)
+
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_update_nonexistent_extra_spec_id(self):
+ """ Should not update volume type extra specs with nonexistent id."""
+ extra_spec = {"spec1": "val2"}
+ self.client.update_volume_type_extra_specs(self.volume_type['id'],
+ str(uuid.uuid4()),
+ extra_spec)
+
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_update_none_extra_spec_id(self):
+ """ Should not update volume type extra specs with none id."""
+ extra_spec = {"spec1": "val2"}
+ self.client.update_volume_type_extra_specs(self.volume_type['id'],
+ None, extra_spec)
+
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_update_multiple_extra_spec(self):
+ """ Should not update volume type extra specs with multiple specs as
+ body.
+ """
+ extra_spec = {"spec1": "val2", 'spec2': 'val1'}
+ self.client.update_volume_type_extra_specs(self.volume_type['id'],
+ extra_spec.keys()[0],
+ extra_spec)
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_create_nonexistent_type_id(self):
+ """ Should not create volume type extra spec for nonexistent volume
+ type id.
+ """
+ extra_specs = {"spec2": "val1"}
+ self.client.create_volume_type_extra_specs(str(uuid.uuid4()),
+ extra_specs)
+
+ @unittest.skip('Until bug 1090322 is fixed')
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_create_none_body(self):
+ """ Should not create volume type extra spec for none POST body."""
+ self.client.create_volume_type_extra_specs(self.volume_type['id'],
+ None)
+
+ @unittest.skip('Until bug 1090322 is fixed')
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_create_invalid_body(self):
+ """ Should not create volume type extra spec for invalid POST body."""
+ self.client.create_volume_type_extra_specs(self.volume_type['id'],
+ ['invalid'])
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_delete_nonexistent_volume_type_id(self):
+ """ Should not delete volume type extra spec for nonexistent
+ type id.
+ """
+ extra_specs = {"spec1": "val1"}
+ self.client.delete_volume_type_extra_specs(str(uuid.uuid4()),
+ extra_specs.keys()[0])
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_list_nonexistent_volume_type_id(self):
+ """ Should not list volume type extra spec for nonexistent type id."""
+ self.client.list_volume_types_extra_specs(str(uuid.uuid4()))
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_get_nonexistent_volume_type_id(self):
+ """ Should not get volume type extra spec for nonexistent type id."""
+ extra_specs = {"spec1": "val1"}
+ self.client.get_volume_type_extra_specs(str(uuid.uuid4()),
+ extra_specs.keys()[0])
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_get_nonexistent_extra_spec_id(self):
+ """ Should not get volume type extra spec for nonexistent extra spec
+ id.
+ """
+ self.client.get_volume_type_extra_specs(self.volume_type['id'],
+ str(uuid.uuid4()))
+
+
+class ExtraSpecsNegativeTestXML(BaseVolumeAdminTestXML,
+ ExtraSpecsNegativeTestBase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(ExtraSpecsNegativeTestXML, cls).setUpClass()
+ ExtraSpecsNegativeTestBase.setUpClass(cls)
+
+ @classmethod
+ def tearDownClass(cls):
+ super(ExtraSpecsNegativeTestXML, cls).tearDownClass()
+ ExtraSpecsNegativeTestBase.tearDownClass(cls)
+
+
+class ExtraSpecsNegativeTestJSON(BaseVolumeAdminTestJSON,
+ ExtraSpecsNegativeTestBase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(ExtraSpecsNegativeTestJSON, cls).setUpClass()
+ ExtraSpecsNegativeTestBase.setUpClass(cls)
+
+ @classmethod
+ def tearDownClass(cls):
+ super(ExtraSpecsNegativeTestJSON, cls).tearDownClass()
+ ExtraSpecsNegativeTestBase.tearDownClass(cls)
diff --git a/tempest/tests/volume/admin/test_volume_types_negative.py b/tempest/tests/volume/admin/test_volume_types_negative.py
new file mode 100644
index 0000000..91237fc
--- /dev/null
+++ b/tempest/tests/volume/admin/test_volume_types_negative.py
@@ -0,0 +1,78 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack, LLC
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import unittest
+import uuid
+
+from nose.plugins.attrib import attr
+from nose.tools import raises
+
+from tempest import exceptions
+from tempest.tests.volume.admin.base import BaseVolumeAdminTestJSON
+from tempest.tests.volume.admin.base import BaseVolumeAdminTestXML
+
+
+class VolumeTypesNegativeTestBase():
+
+ @staticmethod
+ def setUpClass(cls):
+ cls.client = cls.client
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_create_with_nonexistent_volume_type(self):
+ """ Should not be able to create volume with nonexistent volume_type.
+ """
+ self.volumes_client.create_volume(size=1,
+ display_name=str(uuid.uuid4()),
+ volume_type=str(uuid.uuid4()))
+
+ @unittest.skip('Until bug 1090356 is fixed')
+ @raises(exceptions.BadRequest)
+ @attr(type='negative')
+ def test_create_with_empty_name(self):
+ """ Should not be able to create volume type with an empty name."""
+ self.client.create_volume_type('')
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_get_nonexistent_type_id(self):
+ """ Should not be able to get volume type with nonexistent type id."""
+ self.client.get_volume_type(str(uuid.uuid4()))
+
+ @raises(exceptions.NotFound)
+ @attr(type='negative')
+ def test_delete_nonexistent_type_id(self):
+ """ Should not be able to delete volume type with nonexistent type id.
+ """
+ self.client.delete_volume_type(str(uuid.uuid4()))
+
+
+class VolumesTypesNegativeTestXML(BaseVolumeAdminTestXML,
+ VolumeTypesNegativeTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super(VolumesTypesNegativeTestXML, cls).setUpClass()
+ VolumeTypesNegativeTestBase.setUpClass(cls)
+
+
+class VolumesTypesNegativeTestJSON(BaseVolumeAdminTestJSON,
+ VolumeTypesNegativeTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super(VolumesTypesNegativeTestJSON, cls).setUpClass()
+ VolumeTypesNegativeTestBase.setUpClass(cls)