Merge "Configure a heat security group for testing ssh."
diff --git a/tempest/api/compute/images/test_images_oneserver.py b/tempest/api/compute/images/test_images_oneserver.py
index 4163245..7740cfc 100644
--- a/tempest/api/compute/images/test_images_oneserver.py
+++ b/tempest/api/compute/images/test_images_oneserver.py
@@ -40,7 +40,6 @@
     def setUpClass(cls):
         super(ImagesOneServerTestJSON, cls).setUpClass()
         cls.client = cls.images_client
-        cls.servers_client = cls.servers_client
 
         try:
             resp, cls.server = cls.create_server(wait_until='ACTIVE')
@@ -104,6 +103,10 @@
         self.assertRaises(exceptions.NotFound,
                           self.alt_client.delete_image, image_id)
 
+    def _get_default_flavor_disk_size(self, flavor_id):
+        resp, flavor = self.flavors_client.get_flavor_details(flavor_id)
+        return flavor['disk']
+
     @testtools.skipUnless(compute.CREATE_IMAGE_ENABLED,
                           'Environment unable to create images.')
     @attr(type='smoke')
@@ -123,10 +126,15 @@
         self.assertEqual(name, image['name'])
         self.assertEqual('test', image['metadata']['image_type'])
 
-        # Verify minRAM and minDisk values are the same as the original image
         resp, original_image = self.client.get_image(self.image_ref)
-        self.assertEqual(original_image['minRam'], image['minRam'])
-        self.assertEqual(original_image['minDisk'], image['minDisk'])
+
+        # Verify minRAM is the same as the original image
+        self.assertEqual(image['minRam'], original_image['minRam'])
+
+        # Verify minDisk is the same as the original image or the flavor size
+        flavor_disk_size = self._get_default_flavor_disk_size(self.flavor_ref)
+        self.assertIn(str(image['minDisk']),
+                      (str(original_image['minDisk']), str(flavor_disk_size)))
 
         # Verify the image was deleted correctly
         resp, body = self.client.delete_image(image_id)
diff --git a/tempest/api/identity/admin/v3/test_domains.py b/tempest/api/identity/admin/v3/test_domains.py
index 8d019fe..3d40eb3 100644
--- a/tempest/api/identity/admin/v3/test_domains.py
+++ b/tempest/api/identity/admin/v3/test_domains.py
@@ -15,6 +15,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+
 from tempest.api.identity import base
 from tempest.common.utils.data_utils import rand_name
 from tempest.test import attr
@@ -49,6 +50,48 @@
         missing_doms = [d for d in domain_ids if d not in fetched_ids]
         self.assertEqual(0, len(missing_doms))
 
+    @attr(type='smoke')
+    def test_create_update_delete_domain(self):
+        d_name = rand_name('domain-')
+        d_desc = rand_name('domain-desc-')
+        resp_1, domain = self.v3_client.create_domain(
+            d_name, description=d_desc)
+        self.assertEqual(resp_1['status'], '201')
+        self.addCleanup(self._delete_domain, domain['id'])
+        self.assertIn('id', domain)
+        self.assertIn('description', domain)
+        self.assertIn('name', domain)
+        self.assertIn('enabled', domain)
+        self.assertIn('links', domain)
+        self.assertIsNotNone(domain['id'])
+        self.assertEqual(d_name, domain['name'])
+        self.assertEqual(d_desc, domain['description'])
+        if self._interface == "json":
+            self.assertEqual(True, domain['enabled'])
+        else:
+            self.assertEqual('true', str(domain['enabled']).lower())
+        new_desc = rand_name('new-desc-')
+        new_name = rand_name('new-name-')
+
+        resp_2, updated_domain = self.v3_client.update_domain(
+            domain['id'], name=new_name, description=new_desc)
+        self.assertEqual(resp_2['status'], '200')
+        self.assertIn('id', updated_domain)
+        self.assertIn('description', updated_domain)
+        self.assertIn('name', updated_domain)
+        self.assertIn('enabled', updated_domain)
+        self.assertIn('links', updated_domain)
+        self.assertIsNotNone(updated_domain['id'])
+        self.assertEqual(new_name, updated_domain['name'])
+        self.assertEqual(new_desc, updated_domain['description'])
+        self.assertEqual('true', str(updated_domain['enabled']).lower())
+
+        resp_3, fetched_domain = self.v3_client.get_domain(domain['id'])
+        self.assertEqual(resp_3['status'], '200')
+        self.assertEqual(new_name, fetched_domain['name'])
+        self.assertEqual(new_desc, fetched_domain['description'])
+        self.assertEqual('true', str(fetched_domain['enabled']).lower())
+
 
 class DomainsTestXML(DomainsTestJSON):
     _interface = 'xml'
diff --git a/tempest/api/volume/test_volumes_actions.py b/tempest/api/volume/test_volumes_actions.py
index cd5ab34..56a3006 100644
--- a/tempest/api/volume/test_volumes_actions.py
+++ b/tempest/api/volume/test_volumes_actions.py
@@ -27,7 +27,7 @@
     def setUpClass(cls):
         super(VolumesActionsTest, cls).setUpClass()
         cls.client = cls.volumes_client
-        cls.servers_client = cls.servers_client
+        cls.image_client = cls.os.image_client
 
         # Create a test shared instance and volume for attach/detach tests
         srv_name = rand_name('Instance-')
@@ -93,3 +93,16 @@
         finally:
             self.client.detach_volume(self.volume['id'])
             self.client.wait_for_volume_status(self.volume['id'], 'available')
+
+    @attr(type='gate')
+    def test_volume_upload(self):
+        # NOTE(gfidente): the volume uploaded in Glance comes from setUpClass,
+        # it is shared with the other tests. After it is uploaded in Glance,
+        # there is no way to delete it from Cinder, so we delete it from Glance
+        # using the Glance image_client and from Cinder via tearDownClass.
+        image_name = rand_name('Image-')
+        resp, body = self.client.upload_volume(self.volume['id'], image_name)
+        image_id = body["image_id"]
+        self.addCleanup(self.image_client.delete_image, image_id)
+        self.assertEqual(202, resp.status)
+        self.image_client.wait_for_image_status(image_id, 'active')
diff --git a/tempest/cli/__init__.py b/tempest/cli/__init__.py
index 90a1520..0e1d6db 100644
--- a/tempest/cli/__init__.py
+++ b/tempest/cli/__init__.py
@@ -77,6 +77,11 @@
         return self.cmd_with_auth(
             'glance', action, flags, params, admin, fail_ok)
 
+    def cinder(self, action, flags='', params='', admin=True, fail_ok=False):
+        """Executes cinder command for the given action."""
+        return self.cmd_with_auth(
+            'cinder', action, flags, params, admin, fail_ok)
+
     def cmd_with_auth(self, cmd, action, flags='', params='',
                       admin=True, fail_ok=False):
         """Executes given command with auth attributes appended."""
diff --git a/tempest/cli/simple_read_only/test_cinder.py b/tempest/cli/simple_read_only/test_cinder.py
new file mode 100644
index 0000000..e9ce87b
--- /dev/null
+++ b/tempest/cli/simple_read_only/test_cinder.py
@@ -0,0 +1,117 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 OpenStack Foundation
+# 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 logging
+import re
+import subprocess
+
+import tempest.cli
+
+LOG = logging.getLogger(__name__)
+
+
+class SimpleReadOnlyCinderClientTest(tempest.cli.ClientTestBase):
+    """Basic, read-only tests for Cinder CLI client.
+
+    Checks return values and output of read-only commands.
+    These tests do not presume any content, nor do they create
+    their own. They only verify the structure of output if present.
+    """
+
+    def test_cinder_fake_action(self):
+        self.assertRaises(subprocess.CalledProcessError,
+                          self.cinder,
+                          'this-does-not-exist')
+
+    def test_cinder_absolute_limit_list(self):
+        roles = self.parser.listing(self.cinder('absolute-limits'))
+        self.assertTableStruct(roles, ['Name', 'Value'])
+
+    def test_cinder_backup_list(self):
+        self.cinder('backup-list')
+
+    def test_cinder_extra_specs_list(self):
+        self.cinder('extra-specs-list')
+
+    def test_cinder_volumes_list(self):
+        self.cinder('list')
+
+    def test_cinder_quota_class_show(self):
+        """This CLI can accept and string as param."""
+        roles = self.parser.listing(self.cinder('quota-class-show',
+                                                params='abc'))
+        self.assertTableStruct(roles, ['Property', 'Value'])
+
+    def test_cinder_quota_defaults(self):
+        """This CLI can accept and string as param."""
+        roles = self.parser.listing(self.cinder('quota-defaults',
+                                                params=self.identity.
+                                                admin_tenant_name))
+        self.assertTableStruct(roles, ['Property', 'Value'])
+
+    def test_cinder_quota_show(self):
+        """This CLI can accept and string as param."""
+        roles = self.parser.listing(self.cinder('quota-show',
+                                                params=self.identity.
+                                                admin_tenant_name))
+        self.assertTableStruct(roles, ['Property', 'Value'])
+
+    def test_cinder_rate_limits(self):
+        self.cinder('rate-limits')
+
+    def test_cinder_snapshot_list(self):
+        self.cinder('snapshot-list')
+
+    def test_cinder_type_list(self):
+        self.cinder('type-list')
+
+    def test_cinder_list_extensions(self):
+        self.cinder('list-extensions')
+        roles = self.parser.listing(self.cinder('list-extensions'))
+        self.assertTableStruct(roles, ['Name', 'Summary', 'Alias', 'Updated'])
+
+    def test_admin_help(self):
+        help_text = self.cinder('help')
+        lines = help_text.split('\n')
+        self.assertTrue(lines[0].startswith('usage: cinder'))
+
+        commands = []
+        cmds_start = lines.index('Positional arguments:')
+        cmds_end = lines.index('Optional arguments:')
+        command_pattern = re.compile('^ {4}([a-z0-9\-\_]+)')
+        for line in lines[cmds_start:cmds_end]:
+            match = command_pattern.match(line)
+            if match:
+                commands.append(match.group(1))
+        commands = set(commands)
+        wanted_commands = set(('absolute-limits', 'list', 'help',
+                               'quota-show', 'type-list', 'snapshot-list'))
+        self.assertFalse(wanted_commands - commands)
+
+     # Optional arguments:
+
+    def test_cinder_version(self):
+        self.cinder('', flags='--version')
+
+    def test_cinder_debug_list(self):
+        self.cinder('list', flags='--debug')
+
+    def test_cinder_retries_list(self):
+        self.cinder('list', flags='--retries 3')
+
+    def test_cinder_region_list(self):
+        self.cinder('list', flags='--os-region-name ' + self.identity.region)
diff --git a/tempest/services/volume/json/volumes_client.py b/tempest/services/volume/json/volumes_client.py
index 87c0eba..c22b398 100644
--- a/tempest/services/volume/json/volumes_client.py
+++ b/tempest/services/volume/json/volumes_client.py
@@ -85,6 +85,17 @@
         """Deletes the Specified Volume."""
         return self.delete("volumes/%s" % str(volume_id))
 
+    def upload_volume(self, volume_id, image_name):
+        """Uploads a volume in Glance."""
+        post_body = {
+            'image_name': image_name,
+        }
+        post_body = json.dumps({'os-volume_upload_image': post_body})
+        url = 'volumes/%s/action' % (volume_id)
+        resp, body = self.post(url, post_body, self.headers)
+        body = json.loads(body)
+        return resp, body['os-volume_upload_image']
+
     def attach_volume(self, volume_id, instance_uuid, mountpoint):
         """Attaches a volume to a given instance on a given mountpoint."""
         post_body = {