Merge "Change test_get_default_quotas to use assertEqual"
diff --git a/.testr.conf b/.testr.conf
new file mode 100644
index 0000000..a0262d8
--- /dev/null
+++ b/.testr.conf
@@ -0,0 +1,4 @@
+[DEFAULT]
+test_command=${PYTHON:-python} -m subunit.run discover -t ./ ./tempest $LISTOPT $IDOPTION
+test_id_option=--load-list $IDFILE
+test_list_option=--list
diff --git a/tempest/clients.py b/tempest/clients.py
index 32bf2c3..28abb79 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -57,7 +57,9 @@
 from tempest.services.object_storage.account_client import AccountClient
 from tempest.services.object_storage.container_client import ContainerClient
 from tempest.services.object_storage.object_client import ObjectClient
+from tempest.services.volume.json.snapshots_client import SnapshotsClientJSON
 from tempest.services.volume.json.volumes_client import VolumesClientJSON
+from tempest.services.volume.xml.snapshots_client import SnapshotsClientXML
 from tempest.services.volume.xml.volumes_client import VolumesClientXML
 from tempest.services.object_storage.object_client import \
     ObjectClientCustomizedHeader
@@ -111,6 +113,11 @@
     "xml": FloatingIPsClientXML,
 }
 
+SNAPSHOTS_CLIENTS = {
+    "json": SnapshotsClientJSON,
+    "xml": SnapshotsClientXML,
+}
+
 VOLUMES_CLIENTS = {
     "json": VolumesClientJSON,
     "xml": VolumesClientXML,
@@ -184,6 +191,7 @@
             vol_ext_cli = VOLUMES_EXTENSIONS_CLIENTS[interface](*client_args)
             self.volumes_extensions_client = vol_ext_cli
             self.floating_ips_client = FLOAT_CLIENTS[interface](*client_args)
+            self.snapshots_client = SNAPSHOTS_CLIENTS[interface](*client_args)
             self.volumes_client = VOLUMES_CLIENTS[interface](*client_args)
             self.identity_client = IDENTITY_CLIENT[interface](*client_args)
             self.token_client = TOKEN_CLIENT[interface](self.config)
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index 6b9ce74..fca2d2d 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -86,6 +86,10 @@
     message = "Volume %(volume_id)s failed to build and is in ERROR status"
 
 
+class SnapshotBuildErrorException(TempestException):
+    message = "Snapshot %(snapshot_id)s failed to build and is in ERROR status"
+
+
 class BadRequest(RestClientException):
     message = "Bad request"
 
diff --git a/tempest/manager.py b/tempest/manager.py
index 344b8fb..6f23727 100644
--- a/tempest/manager.py
+++ b/tempest/manager.py
@@ -40,6 +40,7 @@
 from tempest.services.compute.json import servers_client
 from tempest.services.compute.json import volumes_extensions_client
 from tempest.services.network.json import network_client
+from tempest.services.volume.json import snapshots_client
 from tempest.services.volume.json import volumes_client
 
 NetworkClient = network_client.NetworkClient
@@ -53,6 +54,7 @@
 KeyPairsClient = keypairs_client.KeyPairsClientJSON
 VolumesExtensionsClient = volumes_extensions_client.VolumesExtensionsClientJSON
 VolumesClient = volumes_client.VolumesClientJSON
+SnapshotsClient = snapshots_client.SnapshotsClientJSON
 QuotasClient = quotas_client.QuotasClientJSON
 
 LOG = logging.getLogger(__name__)
@@ -252,6 +254,7 @@
         self.floating_ips_client = FloatingIPsClient(*client_args)
         self.volumes_extensions_client = VolumesExtensionsClient(*client_args)
         self.volumes_client = VolumesClient(*client_args)
+        self.snapshots_client = SnapshotsClient(*client_args)
         self.quotas_client = QuotasClient(*client_args)
         self.network_client = NetworkClient(*client_args)
 
diff --git a/tempest/services/volume/json/snapshots_client.py b/tempest/services/volume/json/snapshots_client.py
new file mode 100644
index 0000000..9545d0b
--- /dev/null
+++ b/tempest/services/volume/json/snapshots_client.py
@@ -0,0 +1,125 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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 logging
+import time
+import urllib
+
+from tempest.common.rest_client import RestClient
+from tempest import exceptions
+
+LOG = logging.getLogger(__name__)
+
+
+class SnapshotsClientJSON(RestClient):
+    """Client class to send CRUD Volume API requests."""
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(SnapshotsClientJSON, self).__init__(config, username, password,
+                                                  auth_url, tenant_name)
+
+        self.service = self.config.volume.catalog_type
+        self.build_interval = self.config.volume.build_interval
+        self.build_timeout = self.config.volume.build_timeout
+
+    def list_snapshots(self, params=None):
+        """List all the snapshot."""
+        url = 'snapshots'
+        if params:
+                url += '?%s' % urllib.urlencode(params)
+
+        resp, body = self.get(url)
+        body = json.loads(body)
+        return resp, body['snapshots']
+
+    def list_snapshot_with_detail(self, params=None):
+        """List the details of all snapshots."""
+        url = 'snapshots/detail'
+        if params:
+                url += '?%s' % urllib.urlencode(params)
+
+        resp, body = self.get(url)
+        body = json.loads(body)
+        return resp, body['snapshots']
+
+    def get_snapshot(self, snapshot_id):
+        """Returns the details of a single snapshot."""
+        url = "snapshots/%s" % str(snapshot_id)
+        resp, body = self.get(url)
+        body = json.loads(body)
+        return resp, body['snapshot']
+
+    def create_snapshot(self, volume_id, **kwargs):
+        """
+        Creates a new snapshot.
+        volume_id(Required): id of the volume.
+        force: Create a snapshot even if the volume attached (Default=False)
+        display_name: Optional snapshot Name.
+        display_description: User friendly snapshot description.
+        """
+        post_body = {'volume_id': volume_id}
+        post_body.update(kwargs)
+        post_body = json.dumps({'snapshot': post_body})
+        resp, body = self.post('snapshots', post_body, self.headers)
+        body = json.loads(body)
+        return resp, body['snapshot']
+
+    #NOTE(afazekas): just for the wait function
+    def _get_snapshot_status(self, snapshot_id):
+        resp, body = self.get_snapshot(snapshot_id)
+        status = body['status']
+        #NOTE(afazekas): snapshot can reach an "error"
+        # state in a "normal" lifecycle
+        if (status == 'error'):
+            raise exceptions.SnapshotBuildErrorException(
+                    snapshot_id=snapshot_id)
+
+        return status
+
+    #NOTE(afazkas): Wait reinvented again. It is not in the correct layer
+    def wait_for_snapshot_status(self, snapshot_id, status):
+        """Waits for a Snapshot to reach a given status."""
+        start_time = time.time()
+        old_value = value = self._get_snapshot_status(snapshot_id)
+        while True:
+            dtime = time.time() - start_time
+            time.sleep(self.build_interval)
+            if value != old_value:
+                LOG.info('Value transition from "%s" to "%s"'
+                         'in %d second(s).', old_value,
+                         value, dtime)
+            if (value == status):
+                return value
+
+            if dtime > self.build_timeout:
+                message = ('Time Limit Exceeded! (%ds)'
+                           'while waiting for %s, '
+                           'but we got %s.' %
+                           (self.build_timeout, status, value))
+                raise exceptions.TimeoutException(message)
+            time.sleep(self.build_interval)
+            old_value = value
+            value = self._get_snapshot_status(snapshot_id)
+
+    def delete_snapshot(self, snapshot_id):
+        """Delete Snapshot."""
+        return self.delete("snapshots/%s" % str(snapshot_id))
+
+    def is_resource_deleted(self, id):
+        try:
+            self.get_snapshot(id)
+        except exceptions.NotFound:
+            return True
+        return False
diff --git a/tempest/services/volume/json/volumes_client.py b/tempest/services/volume/json/volumes_client.py
index cc5a115..75e1a8b 100644
--- a/tempest/services/volume/json/volumes_client.py
+++ b/tempest/services/volume/json/volumes_client.py
@@ -71,14 +71,10 @@
         display_name: Optional Volume Name.
         metadata: A dictionary of values to be used as metadata.
         volume_type: Optional Name of volume_type for the volume
+        snapshot_id: When specified the volume is created from this snapshot
         """
-        post_body = {
-            'size': size,
-            'display_name': kwargs.get('display_name'),
-            'metadata': kwargs.get('metadata'),
-            'volume_type': kwargs.get('volume_type')
-        }
-
+        post_body = {'size': size}
+        post_body.update(kwargs)
         post_body = json.dumps({'volume': post_body})
         resp, body = self.post('volumes', post_body, self.headers)
         body = json.loads(body)
diff --git a/tempest/services/volume/xml/snapshots_client.py b/tempest/services/volume/xml/snapshots_client.py
new file mode 100644
index 0000000..c89f66e
--- /dev/null
+++ b/tempest/services/volume/xml/snapshots_client.py
@@ -0,0 +1,138 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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 logging
+import time
+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 xml_to_json
+from tempest.services.compute.xml.common import XMLNS_11
+
+LOG = logging.getLogger(__name__)
+
+
+class SnapshotsClientXML(RestClientXML):
+    """Client class to send CRUD Volume API requests."""
+
+    def __init__(self, config, username, password, auth_url, tenant_name=None):
+        super(SnapshotsClientXML, self).__init__(config, username, password,
+                                                 auth_url, tenant_name)
+
+        self.service = self.config.volume.catalog_type
+        self.build_interval = self.config.volume.build_interval
+        self.build_timeout = self.config.volume.build_timeout
+
+    def list_snapshots(self, params=None):
+        """List all snapshot."""
+        url = 'snapshots'
+
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+
+        resp, body = self.get(url, self.headers)
+        body = etree.fromstring(body)
+        return resp, xml_to_json(body)
+
+    def list_snapshots_with_detail(self, params=None):
+        """List all the details of snapshot."""
+        url = 'snapshots/detail'
+
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+
+        resp, body = self.get(url, self.headers)
+        body = etree.fromstring(body)
+        snapshots = []
+        return resp, snapshots(xml_to_json(body))
+
+    def get_snapshot(self, snapshot_id):
+        """Returns the details of a single snapshot."""
+        url = "snapshots/%s" % str(snapshot_id)
+        resp, body = self.get(url, self.headers)
+        body = etree.fromstring(body)
+        return resp, xml_to_json(body)
+
+    def create_snapshot(self, volume_id, **kwargs):
+        """ Creates a new snapshot.
+        volume_id(Required): id of the volume.
+        force: Create a snapshot even if the volume attached (Default=False)
+        display_name: Optional snapshot Name.
+        display_description: User friendly snapshot description.
+        """
+        #NOTE(afazekas): it should use the volume namaspace
+        snapshot = Element("snapshot", xmlns=XMLNS_11, volume_id=volume_id)
+        for key, value in kwargs.items():
+            snapshot.add_attr(key, value)
+        resp, body = self.post('snapshots', str(Document(snapshot)),
+                               self.headers)
+        body = xml_to_json(etree.fromstring(body))
+        return resp, body
+
+    def _get_snapshot_status(self, snapshot_id):
+        resp, body = self.get_snapshot(snapshot_id)
+        return body['status']
+
+    #NOTE(afazekas): just for the wait function
+    def _get_snapshot_status(self, snapshot_id):
+        resp, body = self.get_snapshot(snapshot_id)
+        status = body['status']
+        #NOTE(afazekas): snapshot can reach an "error"
+        # state in a "normal" lifecycle
+        if (status == 'error'):
+            raise exceptions.SnapshotBuildErrorException(
+                    snapshot_id=snapshot_id)
+
+        return status
+
+    #NOTE(afazkas): Wait reinvented again. It is not in the correct layer
+    def wait_for_snapshot_status(self, snapshot_id, status):
+        """Waits for a Snapshot to reach a given status."""
+        start_time = time.time()
+        old_value = value = self._get_snapshot_status(snapshot_id)
+        while True:
+            dtime = time.time() - start_time
+            time.sleep(self.build_interval)
+            if value != old_value:
+                LOG.info('Value transition from "%s" to "%s"'
+                         'in %d second(s).', old_value,
+                         value, dtime)
+            if (value == status):
+                return value
+
+            if dtime > self.build_timeout:
+                message = ('Time Limit Exceeded! (%ds)'
+                           'while waiting for %s, '
+                           'but we got %s.' %
+                           (self.build_timeout, status, value))
+                raise exceptions.TimeoutException(message)
+            time.sleep(self.build_interval)
+            old_value = value
+            value = self._get_snapshot_status(snapshot_id)
+
+    def delete_snapshot(self, snapshot_id):
+        """Delete Snapshot."""
+        return self.delete("snapshots/%s" % str(snapshot_id))
+
+    def is_resource_deleted(self, id):
+        try:
+            self.get_snapshot(id)
+        except exceptions.NotFound:
+            return True
+        return False
diff --git a/tempest/services/volume/xml/volumes_client.py b/tempest/services/volume/xml/volumes_client.py
index 440a80e..862ffae 100644
--- a/tempest/services/volume/xml/volumes_client.py
+++ b/tempest/services/volume/xml/volumes_client.py
@@ -101,6 +101,7 @@
         :param snapshot_id: When specified the volume is created from
                             this snapshot
         """
+        #NOTE(afazekas): it should use a volume namespace
         volume = Element("volume", xmlns=XMLNS_11, size=size)
 
         if 'metadata' in kwargs:
diff --git a/tempest/tests/compute/admin/test_flavors.py b/tempest/tests/compute/admin/test_flavors.py
index 2967666..7b80011 100644
--- a/tempest/tests/compute/admin/test_flavors.py
+++ b/tempest/tests/compute/admin/test_flavors.py
@@ -20,6 +20,7 @@
 
 from tempest.common.utils.data_utils import rand_int_id
 from tempest.common.utils.data_utils import rand_name
+from tempest import exceptions
 from tempest.tests import compute
 from tempest.tests.compute import base
 
@@ -307,6 +308,12 @@
                 self.assertEqual(resp.status, 202)
                 self.client.wait_for_resource_deletion(flavor_id)
 
+    @attr(type='negative')
+    def test_invalid_is_public_string(self):
+        self.assertRaises(exceptions.BadRequest,
+                          self.client.list_flavors_with_detail,
+                          {'is_public': 'invalid'})
+
 
 class FlavorsAdminTestXML(base.BaseComputeAdminTestXML,
                           base.BaseComputeTestXML,
diff --git a/tempest/tests/volume/base.py b/tempest/tests/volume/base.py
index efa74b5..49918e8 100644
--- a/tempest/tests/volume/base.py
+++ b/tempest/tests/volume/base.py
@@ -49,12 +49,14 @@
 
         cls.os = os
         cls.volumes_client = os.volumes_client
+        cls.snapshots_client = os.snapshots_client
         cls.servers_client = os.servers_client
         cls.image_ref = cls.config.compute.image_ref
         cls.flavor_ref = cls.config.compute.flavor_ref
         cls.build_interval = cls.config.volume.build_interval
         cls.build_timeout = cls.config.volume.build_timeout
-        cls.volumes = {}
+        cls.snapshots = []
+        cls.volumes = []
 
         skip_msg = ("%s skipped as Cinder endpoint is not available" %
                     cls.__name__)
@@ -119,19 +121,61 @@
 
     @classmethod
     def tearDownClass(cls):
+        cls.clear_snapshots()
+        cls.clear_volumes()
         cls.clear_isolated_creds()
 
-    def create_volume(self, size=1, metadata={}):
+    @classmethod
+    def create_snapshot(cls, volume_id=1, **kwargs):
+        """Wrapper utility that returns a test snapshot."""
+        resp, snapshot = cls.snapshots_client.create_snapshot(volume_id,
+                                                              **kwargs)
+        assert 200 == resp.status
+        cls.snapshots_client.wait_for_snapshot_status(snapshot['id'],
+                                                      'available')
+        cls.snapshots.append(snapshot)
+        return snapshot
+
+    #NOTE(afazekas): these create_* and clean_* could be defined
+    # only in a single location in the source, and could be more general.
+
+    @classmethod
+    def create_volume(cls, size=1, **kwargs):
         """Wrapper utility that returns a test volume."""
-        display_name = rand_name(self.__class__.__name__ + "-volume")
-        cli_resp = self.volumes_client.create_volume(size=size,
-                                                     display_name=display_name,
-                                                     metdata=metadata)
-        resp, volume = cli_resp
-        self.volumes_client.wait_for_volume_status(volume['id'], 'available')
-        self.volumes.append(volume)
+        resp, volume = cls.volumes_client.create_volume(size, **kwargs)
+        assert 200 == resp.status
+        cls.volumes_client.wait_for_volume_status(volume['id'], 'available')
+        cls.volumes.append(volume)
         return volume
 
+    @classmethod
+    def clear_volumes(cls):
+        for volume in cls.volumes:
+            try:
+                cls.volume_client.delete_volume(volume['id'])
+            except Exception:
+                pass
+
+        for volume in cls.volumes:
+            try:
+                cls.servers_client.wait_for_resource_deletion(volume['id'])
+            except Exception:
+                pass
+
+    @classmethod
+    def clear_snapshots(cls):
+        for snapshot in cls.snapshots:
+            try:
+                cls.snapshots_client.delete_snapshot(snapshot['id'])
+            except Exception:
+                pass
+
+        for snapshot in cls.snapshots:
+            try:
+                cls.snapshots_client.wait_for_resource_deletion(snapshot['id'])
+            except Exception:
+                pass
+
     def wait_for(self, condition):
         """Repeatedly calls condition() until a timeout."""
         start_time = int(time.time())
diff --git a/tempest/tests/volume/test_volumes_snapshots.py b/tempest/tests/volume/test_volumes_snapshots.py
new file mode 100644
index 0000000..1e87525
--- /dev/null
+++ b/tempest/tests/volume/test_volumes_snapshots.py
@@ -0,0 +1,55 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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 nose.plugins.attrib import attr
+
+from tempest.common.utils.data_utils import rand_name
+from tempest.tests.volume import base
+
+
+class VolumesSnapshotTestBase(object):
+
+    def test_volume_from_snapshot(self):
+        volume_origin = self.create_volume(size=1)
+        snapshot = self.create_snapshot(volume_origin['id'])
+        volume_snap = self.create_volume(size=1,
+                                         snapshot_id=
+                                         snapshot['id'])
+        self.snapshots_client.delete_snapshot(snapshot['id'])
+        self.client.delete_volume(volume_snap['id'])
+        self.snapshots_client.wait_for_resource_deletion(snapshot['id'])
+        self.snapshots.remove(snapshot)
+        self.client.delete_volume(volume_origin['id'])
+        self.client.wait_for_resource_deletion(volume_snap['id'])
+        self.volumes.remove(volume_snap)
+        self.client.wait_for_resource_deletion(volume_origin['id'])
+        self.volumes.remove(volume_origin)
+
+
+class VolumesSnapshotTestXML(base.BaseVolumeTestXML,
+                             VolumesSnapshotTestBase):
+    @classmethod
+    def setUpClass(cls):
+        cls._interface = "xml"
+        super(VolumesSnapshotTestXML, cls).setUpClass()
+        cls.client = cls.volumes_client
+
+
+class VolumesSnapshotTestJSON(base.BaseVolumeTestJSON,
+                              VolumesSnapshotTestBase):
+    @classmethod
+    def setUpClass(cls):
+        cls._interface = "json"
+        super(VolumesSnapshotTestJSON, cls).setUpClass()
+        cls.client = cls.volumes_client
diff --git a/tools/pip-requires b/tools/pip-requires
index 0147cd8..5c45a49 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -12,3 +12,4 @@
 python-quantumclient>=2.1
 testresources
 keyring
+testrepository