Merge "Add client response checking for image service"
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index f1712ac..bf0b308 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -119,13 +119,13 @@
 
 # Timeout for association of Nova instance and Ironic node
 # (integer value)
-#association_timeout=10
+#association_timeout=30
 
 # Timeout for Ironic power transitions. (integer value)
-#power_timeout=20
+#power_timeout=60
 
 # Timeout for unprovisioning an Ironic node. (integer value)
-#unprovision_timeout=20
+#unprovision_timeout=60
 
 
 [boto]
diff --git a/tempest/api/object_storage/test_object_expiry.py b/tempest/api/object_storage/test_object_expiry.py
index 53ca20d..73b4f3b 100644
--- a/tempest/api/object_storage/test_object_expiry.py
+++ b/tempest/api/object_storage/test_object_expiry.py
@@ -54,14 +54,18 @@
         self.assertEqual(resp['status'], '200')
         self.assertHeaders(resp, 'Object', 'HEAD')
         self.assertIn('x-delete-at', resp)
+        # we want to ensure that we will sleep long enough for things to
+        # actually expire, so figure out how many secs in the future that is.
+        sleepy_time = int(resp['x-delete-at']) - int(time.time())
+
         resp, body = self.object_client.get_object(self.container_name,
                                                    self.object_name)
         self.assertEqual(resp['status'], '200')
         self.assertHeaders(resp, 'Object', 'GET')
         self.assertIn('x-delete-at', resp)
 
-        # sleep for over 5 seconds, so that object expires
-        time.sleep(5)
+        # add a couple of seconds for safety.
+        time.sleep(sleepy_time + 3)
 
         # object should not be there anymore
         self.assertRaises(exceptions.NotFound, self.object_client.get_object,
@@ -69,10 +73,12 @@
 
     @test.attr(type='gate')
     def test_get_object_after_expiry_time(self):
-        metadata = {'X-Delete-After': '3'}
+        # the 10s is important, because the get calls can take 3s each
+        # some times
+        metadata = {'X-Delete-After': '10'}
         self._test_object_expiry(metadata)
 
     @test.attr(type='gate')
     def test_get_object_at_expiry_time(self):
-        metadata = {'X-Delete-At': str(int(time.time()) + 3)}
+        metadata = {'X-Delete-At': str(int(time.time()) + 10)}
         self._test_object_expiry(metadata)
diff --git a/tempest/api_schema/compute/agents.py b/tempest/api_schema/compute/agents.py
index b04cf64..b9ad240 100644
--- a/tempest/api_schema/compute/agents.py
+++ b/tempest/api_schema/compute/agents.py
@@ -38,3 +38,24 @@
         'required': ['agents']
     }
 }
+
+common_create_agent = {
+    'type': 'object',
+    'properties': {
+        'agent': {
+            'type': 'object',
+            'properties': {
+                'agent_id': {'type': ['integer', 'string']},
+                'hypervisor': {'type': 'string'},
+                'os': {'type': 'string'},
+                'architecture': {'type': 'string'},
+                'version': {'type': 'string'},
+                'url': {'type': 'string', 'format': 'uri'},
+                'md5hash': {'type': 'string'}
+            },
+            'required': ['agent_id', 'hypervisor', 'os', 'architecture',
+                         'version', 'url', 'md5hash']
+        }
+    },
+    'required': ['agent']
+}
diff --git a/tempest/api_schema/compute/v2/agents.py b/tempest/api_schema/compute/v2/agents.py
index 837731f..30f999f 100644
--- a/tempest/api_schema/compute/v2/agents.py
+++ b/tempest/api_schema/compute/v2/agents.py
@@ -12,6 +12,13 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from tempest.api_schema.compute import agents
+
+create_agent = {
+    'status_code': [200],
+    'response_body': agents.common_create_agent
+}
+
 delete_agent = {
     'status_code': [200]
 }
diff --git a/tempest/api_schema/compute/v3/agents.py b/tempest/api_schema/compute/v3/agents.py
index 63d1c46..597a089 100644
--- a/tempest/api_schema/compute/v3/agents.py
+++ b/tempest/api_schema/compute/v3/agents.py
@@ -12,6 +12,13 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from tempest.api_schema.compute import agents
+
+create_agent = {
+    'status_code': [201],
+    'response_body': agents.common_create_agent
+}
+
 delete_agent = {
     'status_code': [204]
 }
diff --git a/tempest/cli/__init__.py b/tempest/cli/__init__.py
index 6aa98c4..0571f4f 100644
--- a/tempest/cli/__init__.py
+++ b/tempest/cli/__init__.py
@@ -83,6 +83,12 @@
         return self.cmd_with_auth(
             'cinder', action, flags, params, admin, fail_ok)
 
+    def swift(self, action, flags='', params='', admin=True, fail_ok=False):
+        """Executes swift command for the given action."""
+        flags += ' --os-endpoint-type %s' % CONF.object_storage.endpoint_type
+        return self.cmd_with_auth(
+            'swift', action, flags, params, admin, fail_ok)
+
     def neutron(self, action, flags='', params='', admin=True, fail_ok=False):
         """Executes neutron command for the given action."""
         flags += ' --endpoint-type %s' % CONF.network.endpoint_type
diff --git a/tempest/cli/simple_read_only/test_swift.py b/tempest/cli/simple_read_only/test_swift.py
new file mode 100644
index 0000000..6d6caa7
--- /dev/null
+++ b/tempest/cli/simple_read_only/test_swift.py
@@ -0,0 +1,95 @@
+# Copyright 2014 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 re
+import subprocess
+
+import tempest.cli
+from tempest import config
+
+CONF = config.CONF
+
+
+class SimpleReadOnlySwiftClientTest(tempest.cli.ClientTestBase):
+    """Basic, read-only tests for Swift 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.
+    """
+
+    @classmethod
+    def setUpClass(cls):
+        if not CONF.service_available.swift:
+            msg = ("%s skipped as Swift is not available" % cls.__name__)
+            raise cls.skipException(msg)
+        super(SimpleReadOnlySwiftClientTest, cls).setUpClass()
+
+    def test_swift_fake_action(self):
+        self.assertRaises(subprocess.CalledProcessError,
+                          self.swift,
+                          'this-does-not-exist')
+
+    def test_swift_list(self):
+        self.swift('list')
+
+    def test_swift_stat(self):
+        output = self.swift('stat')
+        entries = ['Account', 'Containers', 'Objects', 'Bytes', 'Content-Type',
+                   'X-Timestamp', 'X-Trans-Id']
+        for entry in entries:
+            self.assertTrue(entry in output)
+
+    def test_swift_capabilities(self):
+        output = self.swift('capabilities')
+        entries = ['account_listing_limit', 'container_listing_limit',
+                   'max_file_size', 'Additional middleware']
+        for entry in entries:
+            self.assertTrue(entry in output)
+
+    def test_swift_help(self):
+        help_text = self.swift('', flags='--help')
+        lines = help_text.split('\n')
+        self.assertFirstLineStartsWith(lines, 'Usage: swift')
+
+        commands = []
+        cmds_start = lines.index('Positional arguments:')
+        cmds_end = lines.index('Examples:')
+        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(('stat', 'list', 'delete',
+                               'download', 'post', 'upload'))
+        self.assertFalse(wanted_commands - commands)
+
+    # Optional arguments:
+
+    def test_swift_version(self):
+        self.swift('', flags='--version')
+
+    def test_swift_debug_list(self):
+        self.swift('list', flags='--debug')
+
+    def test_swift_retries_list(self):
+        self.swift('list', flags='--retries 3')
+
+    def test_swift_region_list(self):
+        region = CONF.object_storage.region
+        if not region:
+            region = CONF.identity.region
+        self.swift('list', flags='--os-region-name ' + region)
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index 9273437..9e0f4d3 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -372,7 +372,7 @@
         # The warning is normal for SHOULD/SHOULD NOT case
 
         # Likely it will cause an error
-        if not resp_body and resp.status >= 400:
+        if method != 'HEAD' and not resp_body and resp.status >= 400:
             self.LOG.warning("status >= 400 response with empty body")
 
     def _request(self, method, url, headers=None, body=None):
diff --git a/tempest/config.py b/tempest/config.py
index 4ea0702..7d871cb 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -924,14 +924,14 @@
                default=300,
                help="Timeout for Ironic node to completely provision"),
     cfg.IntOpt('association_timeout',
-               default=10,
+               default=30,
                help="Timeout for association of Nova instance and Ironic "
                     "node"),
     cfg.IntOpt('power_timeout',
-               default=20,
+               default=60,
                help="Timeout for Ironic power transitions."),
     cfg.IntOpt('unprovision_timeout',
-               default=20,
+               default=60,
                help="Timeout for unprovisioning an Ironic node.")
 ]
 
diff --git a/tempest/services/compute/json/agents_client.py b/tempest/services/compute/json/agents_client.py
index 98d8896..4f6602f 100644
--- a/tempest/services/compute/json/agents_client.py
+++ b/tempest/services/compute/json/agents_client.py
@@ -46,7 +46,9 @@
         """Create an agent build."""
         post_body = json.dumps({'agent': kwargs})
         resp, body = self.post('os-agents', post_body)
-        return resp, self._parse_resp(body)
+        body = json.loads(body)
+        self.validate_response(schema.create_agent, resp, body)
+        return resp, body['agent']
 
     def delete_agent(self, agent_id):
         """Delete an existing agent build."""
diff --git a/tempest/services/compute/v3/json/agents_client.py b/tempest/services/compute/v3/json/agents_client.py
index 48be54c..31314b7 100644
--- a/tempest/services/compute/v3/json/agents_client.py
+++ b/tempest/services/compute/v3/json/agents_client.py
@@ -43,7 +43,9 @@
         """Create an agent build."""
         post_body = json.dumps({'agent': kwargs})
         resp, body = self.post('os-agents', post_body)
-        return resp, self._parse_resp(body)
+        body = json.loads(body)
+        self.validate_response(schema.create_agent, resp, body)
+        return resp, body['agent']
 
     def delete_agent(self, agent_id):
         """Delete an existing agent build."""