Merge "Do not limit the max versions in the requirements"
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index 534f3d9..3cbe1b5 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -22,6 +22,8 @@
 # Should typically be left as keystone unless you have a non-Keystone
 # authentication API service
 strategy = keystone
+# The identity region
+region = RegionOne
 
 [compute]
 # This section contains configuration options used when executing tests
@@ -54,6 +56,9 @@
 # The above non-administrative user's tenant name
 alt_tenant_name = alt_demo
 
+# The compute region
+region = RegionOne
+
 # Reference data for tests. The ref and ref_alt should be
 # distinct images/flavors.
 image_ref = {$IMAGE_ID}
@@ -204,13 +209,13 @@
 tenant_networks_reachable = false
 
 # Id of the public network that provides external connectivity.
-public_network_id = {$PUBLIC_NETWORK_UUID}
+public_network_id = {$PUBLIC_NETWORK_ID}
 
 # Id of a shared public router that provides external connectivity.
 # A shared public router would commonly be used where IP namespaces
 # were disabled.  If namespaces are enabled, it would be preferable
 # for each tenant to have their own router.
-public_router_id =
+public_router_id = {$PUBLIC_ROUTER_ID}
 
 [network-admin]
 # This section contains configuration options for an administrative
@@ -261,6 +266,10 @@
 # this value as "object-store"
 catalog_type = object-store
 
+# The object-store region
+region = RegionOne
+
+
 [boto]
 # This section contains configuration options used when executing tests
 # with boto.
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index cb18a9c..884d147 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -44,7 +44,7 @@
         self.token = None
         self.base_url = None
         self.config = config
-        self.region = 0
+        self.region = {'compute': self.config.compute.region}
         self.endpoint_url = 'publicURL'
         self.strategy = self.config.identity.strategy
         self.headers = {'Content-Type': 'application/%s' % self.TYPE,
@@ -142,7 +142,12 @@
             mgmt_url = None
             for ep in auth_data['serviceCatalog']:
                 if ep["type"] == service:
-                    mgmt_url = ep['endpoints'][self.region][self.endpoint_url]
+                    for _ep in ep['endpoints']:
+                        if service in self.region and \
+                                _ep['region'] == self.region[service]:
+                            mgmt_url = _ep[self.endpoint_url]
+                    if not mgmt_url:
+                        mgmt_url = ep['endpoints'][0][self.endpoint_url]
                     tenant_id = auth_data['token']['tenant']['id']
                     break
 
diff --git a/tempest/config.py b/tempest/config.py
index 1d1aa49..fc1bd74 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -96,6 +96,11 @@
         """Which auth method does the environment use? (basic|keystone)"""
         return self.get("strategy", 'keystone')
 
+    @property
+    def region(self):
+        """The identity region name to use."""
+        return self.get("region")
+
 
 class IdentityAdminConfig(BaseConfig):
 
@@ -173,6 +178,11 @@
         return self.get("alt_password")
 
     @property
+    def region(self):
+        """The compute region name to use."""
+        return self.get("region")
+
+    @property
     def image_ref(self):
         """Valid primary image to use in tests."""
         return self.get("image_ref", "{$IMAGE_ID}")
@@ -473,6 +483,11 @@
         """Catalog type of the Object-Storage service."""
         return self.get("catalog_type", 'object-store')
 
+    @property
+    def region(self):
+        """The object-store region name to use."""
+        return self.get("region")
+
 
 class BotoConfig(BaseConfig):
     """Provides configuration information for connecting to EC2/S3."""
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)