Merge "Add XML support to the cinder client"
diff --git a/tempest/manager.py b/tempest/manager.py
index 7a5d6f5..bab7ea7 100644
--- a/tempest/manager.py
+++ b/tempest/manager.py
@@ -49,7 +49,7 @@
 FloatingIPsClient = floating_ips_client.FloatingIPsClientJSON
 KeyPairsClient = keypairs_client.KeyPairsClientJSON
 VolumesExtensionsClient = volumes_extensions_client.VolumesExtensionsClientJSON
-VolumesClient = volumes_client.VolumesClient
+VolumesClient = volumes_client.VolumesClientJSON
 ConsoleOutputsClient = console_output_client.ConsoleOutputsClient
 
 LOG = logging.getLogger(__name__)
diff --git a/tempest/openstack.py b/tempest/openstack.py
index ec0c257..89b7718 100644
--- a/tempest/openstack.py
+++ b/tempest/openstack.py
@@ -45,7 +45,8 @@
 from tempest.services.nova.xml.servers_client import ServersClientXML
 from tempest.services.nova.xml.volumes_extensions_client \
 import VolumesExtensionsClientXML
-from tempest.services.volume.json.volumes_client import VolumesClient
+from tempest.services.volume.json.volumes_client import VolumesClientJSON
+from tempest.services.volume.xml.volumes_client import VolumesClientXML
 
 LOG = logging.getLogger(__name__)
 
@@ -89,6 +90,11 @@
     "xml": FloatingIPsClientXML,
 }
 
+VOLUMES_CLIENTS = {
+    "json": VolumesClientJSON,
+    "xml": VolumesClientXML,
+}
+
 
 class Manager(object):
 
@@ -141,13 +147,13 @@
             self.volumes_extensions_client = \
                     VOLUMES_EXTENSIONS_CLIENTS[interface](*client_args)
             self.floating_ips_client = FLOAT_CLIENTS[interface](*client_args)
+            self.volumes_client = VOLUMES_CLIENTS[interface](*client_args)
         except KeyError:
             msg = "Unsupported interface type `%s'" % interface
             raise exceptions.InvalidConfiguration(msg)
         self.security_groups_client = SecurityGroupsClient(*client_args)
         self.console_outputs_client = ConsoleOutputsClient(*client_args)
         self.network_client = NetworkClient(*client_args)
-        self.volumes_client = VolumesClient(*client_args)
 
 
 class AltManager(Manager):
diff --git a/tempest/services/volume/json/volumes_client.py b/tempest/services/volume/json/volumes_client.py
index 68c7a86..c2e8e45 100644
--- a/tempest/services/volume/json/volumes_client.py
+++ b/tempest/services/volume/json/volumes_client.py
@@ -22,13 +22,13 @@
 from tempest import exceptions
 
 
-class VolumesClient(RestClient):
+class VolumesClientJSON(RestClient):
     """
     Client class to send CRUD Volume API requests to a Cinder endpoint
     """
 
     def __init__(self, config, username, password, auth_url, tenant_name=None):
-        super(VolumesClient, self).__init__(config, username, password,
+        super(VolumesClientJSON, self).__init__(config, username, password,
                                            auth_url, tenant_name)
 
         self.service = self.config.volume.catalog_type
diff --git a/tempest/services/volume/xml/__init__.py b/tempest/services/volume/xml/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/volume/xml/__init__.py
diff --git a/tempest/services/volume/xml/volumes_client.py b/tempest/services/volume/xml/volumes_client.py
new file mode 100644
index 0000000..8bb8bff
--- /dev/null
+++ b/tempest/services/volume/xml/volumes_client.py
@@ -0,0 +1,148 @@
+# 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 time
+
+from lxml import etree
+
+from tempest.common.rest_client import RestClientXML
+from tempest import exceptions
+from tempest.services.nova.xml.common import xml_to_json
+from tempest.services.nova.xml.common import XMLNS_11
+from tempest.services.nova.xml.common import Element
+from tempest.services.nova.xml.common import Text
+from tempest.services.nova.xml.common import Document
+
+
+class VolumesClientXML(RestClientXML):
+    """
+    Client class to send CRUD Volume API requests to a Cinder endpoint
+    """
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(VolumesClientXML, self).__init__(config, username, password,
+                                               auth_url, tenant_name)
+        self.service = self.config.compute.catalog_type
+        self.build_interval = self.config.compute.build_interval
+        self.build_timeout = self.config.compute.build_timeout
+
+    def _parse_volume(self, body):
+        vol = 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 == 'metadata':
+                vol['metadata'] = dict((meta.get('key'),
+                                        meta.text) for meta in list(child))
+            else:
+                vol[tag] = xml_to_json(child)
+            return vol
+
+    def list_volumes(self, params=None):
+        """List all the volumes created"""
+        url = 'volumes'
+
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+
+        resp, body = self.get(url, self.headers)
+        body = etree.fromstring(body)
+        volumes = []
+        if body is not None:
+            volumes += [self._parse_volume(vol) for vol in list(body)]
+        return resp, volumes
+
+    def list_volumes_with_detail(self, params=None):
+        """List all the details of volumes"""
+        url = 'volumes/detail'
+
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+
+        resp, body = self.get(url, self.headers)
+        body = etree.fromstring(body)
+        volumes = []
+        if body is not None:
+            volumes += [self._parse_volume(vol) for vol in list(body)]
+        return resp, volumes
+
+    def get_volume(self, volume_id):
+        """Returns the details of a single volume"""
+        url = "volumes/%s" % str(volume_id)
+        resp, body = self.get(url, self.headers)
+        body = etree.fromstring(body)
+        return resp, self._parse_volume(body)
+
+    def create_volume(self, size, display_name=None, metadata=None):
+        """Creates a new Volume.
+
+        :param size: Size of volume in GB. (Required)
+        :param display_name: Optional Volume Name.
+        :param metadata: An optional dictionary of values for metadata.
+        """
+        volume = Element("volume",
+                          xmlns=XMLNS_11,
+                          size=size)
+        if display_name:
+            volume.add_attr('display_name', display_name)
+
+        if metadata:
+            _metadata = Element('metadata')
+            volume.append(_metadata)
+            for key, value in metadata.items():
+                meta = Element('meta')
+                meta.add_attr('key', key)
+                meta.append(Text(value))
+                _metadata.append(meta)
+
+        resp, body = self.post('volumes', str(Document(volume)),
+                               self.headers)
+        body = xml_to_json(etree.fromstring(body))
+        return resp, body
+
+    def delete_volume(self, volume_id):
+        """Deletes the Specified Volume"""
+        return self.delete("volumes/%s" % str(volume_id))
+
+    def wait_for_volume_status(self, volume_id, status):
+        """Waits for a Volume to reach a given status"""
+        resp, body = self.get_volume(volume_id)
+        volume_name = body['displayName']
+        volume_status = body['status']
+        start = int(time.time())
+
+        while volume_status != status:
+            time.sleep(self.build_interval)
+            resp, body = self.get_volume(volume_id)
+            volume_status = body['status']
+            if volume_status == 'error':
+                raise exceptions.VolumeBuildErrorException(volume_id=volume_id)
+
+            if int(time.time()) - start >= self.build_timeout:
+                message = 'Volume %s failed to reach %s status within '\
+                          'the required time (%s s).' % (volume_name, status,
+                                                         self.build_timeout)
+                raise exceptions.TimeoutException(message)
+
+    def is_resource_deleted(self, id):
+        try:
+            self.get_volume(id)
+        except exceptions.NotFound:
+            return True
+        return False
diff --git a/tempest/tests/volume/base.py b/tempest/tests/volume/base.py
index 41f08fe..4862404 100644
--- a/tempest/tests/volume/base.py
+++ b/tempest/tests/volume/base.py
@@ -147,3 +147,17 @@
                 condition()
                 return
             time.sleep(self.build_interval)
+
+
+class BaseVolumeTestJSON(BaseVolumeTest):
+    @classmethod
+    def setUpClass(cls):
+        cls._interface = "json"
+        super(BaseVolumeTestJSON, cls).setUpClass()
+
+
+class BaseVolumeTestXML(BaseVolumeTest):
+    @classmethod
+    def setUpClass(cls):
+        cls._interface = "xml"
+        super(BaseVolumeTestXML, cls).setUpClass()
diff --git a/tempest/tests/volume/test_volumes_get.py b/tempest/tests/volume/test_volumes_get.py
index 4305c67..03ecb5a 100644
--- a/tempest/tests/volume/test_volumes_get.py
+++ b/tempest/tests/volume/test_volumes_get.py
@@ -18,15 +18,10 @@
 from nose.plugins.attrib import attr
 
 from tempest.common.utils.data_utils import rand_name
-from tempest.tests.volume.base import BaseVolumeTest
+from tempest.tests.volume import base
 
 
-class VolumesGetTest(BaseVolumeTest):
-
-    @classmethod
-    def setUpClass(cls):
-        super(VolumesGetTest, cls).setUpClass()
-        cls.client = cls.volumes_client
+class VolumesGetTestBase(object):
 
     @attr(type='smoke')
     def test_volume_create_get_delete(self):
@@ -97,3 +92,19 @@
                 resp, _ = self.client.delete_volume(volume['id'])
                 self.assertEqual(202, resp.status)
                 self.client.wait_for_resource_deletion(volume['id'])
+
+
+class VolumesGetTestXML(base.BaseVolumeTestXML, VolumesGetTestBase):
+    @classmethod
+    def setUpClass(cls):
+        cls._interface = "xml"
+        super(VolumesGetTestXML, cls).setUpClass()
+        cls.client = cls.volumes_client
+
+
+class VolumesGetTestJSON(base.BaseVolumeTestJSON, VolumesGetTestBase):
+    @classmethod
+    def setUpClass(cls):
+        cls._interface = "json"
+        super(VolumesGetTestJSON, cls).setUpClass()
+        cls.client = cls.volumes_client
diff --git a/tempest/tests/volume/test_volumes_list.py b/tempest/tests/volume/test_volumes_list.py
index 24055af..6c6dac2 100644
--- a/tempest/tests/volume/test_volumes_list.py
+++ b/tempest/tests/volume/test_volumes_list.py
@@ -19,10 +19,10 @@
 from nose.plugins.attrib import attr
 
 from tempest.common.utils.data_utils import rand_name
-from tempest.tests.volume.base import BaseVolumeTest
+from tempest.tests.volume import base
 
 
-class VolumesListTest(BaseVolumeTest):
+class VolumesListTestBase(object):
 
     """
     This test creates a number of 1G volumes. To run successfully,
@@ -32,9 +32,38 @@
     VOLUME_BACKING_FILE_SIZE is atleast 4G in your localrc
     """
 
+    @attr(type='smoke')
+    def test_volume_list(self):
+        """Get a list of Volumes"""
+        # Fetch all volumes
+        resp, fetched_list = self.client.list_volumes()
+        self.assertEqual(200, resp.status)
+        # Now check if all the volumes created in setup are in fetched list
+        missing_vols = [v for v in self.volume_list if v not in fetched_list]
+        self.assertFalse(missing_vols,
+                         "Failed to find volume %s in fetched list"
+                         % ', '.join(m_vol['display_name']
+                                        for m_vol in missing_vols))
+
+    @attr(type='smoke')
+    def test_volume_list_with_details(self):
+        """Get a list of Volumes with details"""
+        # Fetch all Volumes
+        resp, fetched_list = self.client.list_volumes_with_detail()
+        self.assertEqual(200, resp.status)
+        # Verify that all the volumes are returned
+        missing_vols = [v for v in self.volume_list if v not in fetched_list]
+        self.assertFalse(missing_vols,
+                         "Failed to find volume %s in fetched list"
+                         % ', '.join(m_vol['display_name']
+                                        for m_vol in missing_vols))
+
+
+class VolumeListTestXML(base.BaseVolumeTestXML, VolumesListTestBase):
     @classmethod
     def setUpClass(cls):
-        super(VolumesListTest, cls).setUpClass()
+        cls._interface = 'xml'
+        super(VolumeListTestXML, cls).setUpClass()
         cls.client = cls.volumes_client
 
         # Create 3 test volumes
@@ -74,30 +103,51 @@
         for volume in cls.volume_id_list:
             resp, _ = cls.client.delete_volume(volume)
             cls.client.wait_for_resource_deletion(volume)
-        super(VolumesListTest, cls).tearDownClass()
+        super(VolumeListTestXML, cls).tearDownClass()
 
-    @attr(type='smoke')
-    def test_volume_list(self):
-        """Get a list of Volumes"""
-        # Fetch all volumes
-        resp, fetched_list = self.client.list_volumes()
-        self.assertEqual(200, resp.status)
-        # Now check if all the volumes created in setup are in fetched list
-        missing_vols = [v for v in self.volume_list if v not in fetched_list]
-        self.assertFalse(missing_vols,
-                         "Failed to find volume %s in fetched list"
-                         % ', '.join(m_vol['display_name']
-                                        for m_vol in missing_vols))
 
-    @attr(type='smoke')
-    def test_volume_list_with_details(self):
-        """Get a list of Volumes with details"""
-        # Fetch all Volumes
-        resp, fetched_list = self.client.list_volumes_with_detail()
-        self.assertEqual(200, resp.status)
-        # Verify that all the volumes are returned
-        missing_vols = [v for v in self.volume_list if v not in fetched_list]
-        self.assertFalse(missing_vols,
-                         "Failed to find volume %s in fetched list"
-                         % ', '.join(m_vol['display_name']
-                                        for m_vol in missing_vols))
+class VolumeListTestJSON(base.BaseVolumeTestJSON, VolumesListTestBase):
+    @classmethod
+    def setUpClass(cls):
+        cls._interface = 'json'
+        super(VolumeListTestJSON, cls).setUpClass()
+        cls.client = cls.volumes_client
+
+        # Create 3 test volumes
+        cls.volume_list = []
+        cls.volume_id_list = []
+        for i in range(3):
+            v_name = rand_name('volume')
+            metadata = {'Type': 'work'}
+            try:
+                resp, volume = cls.client.create_volume(size=1,
+                                                        display_name=v_name,
+                                                        metadata=metadata)
+                cls.client.wait_for_volume_status(volume['id'],
+                                                   'available')
+                resp, volume = cls.client.get_volume(volume['id'])
+                cls.volume_list.append(volume)
+                cls.volume_id_list.append(volume['id'])
+            except:
+                if cls.volume_list:
+                    # We could not create all the volumes, though we were able
+                    # to create *some* of the volumes. This is typically
+                    # because the backing file size of the volume group is
+                    # too small. So, here, we clean up whatever we did manage
+                    # to create and raise a SkipTest
+                    for volume in cls.volume_id_list:
+                        cls.client.delete_volume(volume)
+                    msg = ("Failed to create ALL necessary volumes to run "
+                           "test. This typically means that the backing file "
+                           "size of the nova-volumes group is too small to "
+                           "create the 3 volumes needed by this test case")
+                    raise nose.SkipTest(msg)
+                raise
+
+    @classmethod
+    def tearDownClass(cls):
+        # Delete the created volumes
+        for volume in cls.volume_id_list:
+            resp, _ = cls.client.delete_volume(volume)
+            cls.client.wait_for_resource_deletion(volume)
+        super(VolumeListTestJSON, cls).tearDownClass()
diff --git a/tempest/tests/volume/test_volumes_negative.py b/tempest/tests/volume/test_volumes_negative.py
index 63b209e..bf7e5f0 100644
--- a/tempest/tests/volume/test_volumes_negative.py
+++ b/tempest/tests/volume/test_volumes_negative.py
@@ -20,15 +20,10 @@
 
 from tempest import exceptions
 from tempest.common.utils.data_utils import rand_name
-from tempest.tests.volume.base import BaseVolumeTest
+from tempest.tests.volume import base
 
 
-class VolumesNegativeTest(BaseVolumeTest):
-
-    @classmethod
-    def setUpClass(cls):
-        super(VolumesNegativeTest, cls).setUpClass()
-        cls.client = cls.volumes_client
+class VolumesNegativeTestBase(object):
 
     @raises(exceptions.NotFound)
     @attr(type='negative')
@@ -131,3 +126,18 @@
         Should not be able to delete volume when empty ID is passed
         """
         resp, volume = self.client.delete_volume('')
+
+
+class VolumesNegativeTestXML(base.BaseVolumeTestXML, VolumesNegativeTestBase):
+    @classmethod
+    def setUpClass(cls):
+        super(VolumesNegativeTestXML, cls).setUpClass()
+        cls.client = cls.volumes_client
+
+
+class VolumesNegativeTestJSON(base.BaseVolumeTestJSON,
+                              VolumesNegativeTestBase):
+    @classmethod
+    def setUpClass(cls):
+        super(VolumesNegativeTestJSON, cls).setUpClass()
+        cls.client = cls.volumes_client