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)