Merge "Validate Volume attributes of Nova POST & GET API"
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index 068a666..d75de90 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -617,6 +617,14 @@
 # (string value)
 #public_router_id=
 
+# Timeout in seconds to wait for network operation to
+# complete. (integer value)
+#build_timeout=300
+
+# Time in seconds between network operation status checks.
+# (integer value)
+#build_interval=10
+
 
 [network-feature-enabled]
 
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index ed63f24..b2f3117 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -439,3 +439,4 @@
         cls.aggregates_admin_client = cls.os_adm.aggregates_v3_client
         cls.hosts_admin_client = cls.os_adm.hosts_v3_client
         cls.quotas_admin_client = cls.os_adm.quotas_v3_client
+        cls.agents_admin_client = cls.os_adm.agents_v3_client
diff --git a/tempest/api/compute/servers/test_server_rescue_negative.py b/tempest/api/compute/servers/test_server_rescue_negative.py
index 277f28f..e027567 100644
--- a/tempest/api/compute/servers/test_server_rescue_negative.py
+++ b/tempest/api/compute/servers/test_server_rescue_negative.py
@@ -45,6 +45,11 @@
             cls.rescue_id, adminPass=rescue_password)
         cls.servers_client.wait_for_server_status(cls.rescue_id, 'RESCUE')
 
+    @classmethod
+    def tearDownClass(cls):
+        cls.delete_volume(cls.volume['id'])
+        super(ServerRescueNegativeTestJSON, cls).tearDownClass()
+
     def _detach(self, server_id, volume_id):
         self.servers_client.detach_volume(server_id, volume_id)
         self.volumes_extensions_client.wait_for_volume_status(volume_id,
diff --git a/tempest/api/compute/v3/admin/test_agents.py b/tempest/api/compute/v3/admin/test_agents.py
new file mode 100644
index 0000000..9d01b71
--- /dev/null
+++ b/tempest/api/compute/v3/admin/test_agents.py
@@ -0,0 +1,91 @@
+# Copyright 2014 NEC Corporation.  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.
+
+from tempest.api.compute import base
+from tempest import test
+
+
+class AgentsAdminV3Test(base.BaseV3ComputeAdminTest):
+
+    """
+    Tests Agents API that require admin privileges
+    """
+
+    @classmethod
+    def setUpClass(cls):
+        super(AgentsAdminV3Test, cls).setUpClass()
+        cls.client = cls.agents_admin_client
+
+    @test.attr(type='gate')
+    def test_create_update_list_delete_agents(self):
+
+        """
+        1. Create 2 agents.
+        2. Update one of the agents.
+        3. List all agent builds.
+        4. List the agent builds by the filter.
+        5. Delete agents.
+        """
+        params_kvm = expected_kvm = {'hypervisor': 'kvm',
+                                     'os': 'win',
+                                     'architecture': 'x86',
+                                     'version': '7.0',
+                                     'url': 'xxx://xxxx/xxx/xxx',
+                                     'md5hash': ("""add6bb58e139be103324d04d"""
+                                                 """82d8f545""")}
+
+        resp, agent_kvm = self.client.create_agent(**params_kvm)
+        self.assertEqual(201, resp.status)
+        for expected_item, value in expected_kvm.items():
+            self.assertEqual(value, agent_kvm[expected_item])
+
+        params_xen = expected_xen = {'hypervisor': 'xen',
+                                     'os': 'linux',
+                                     'architecture': 'x86',
+                                     'version': '7.0',
+                                     'url': 'xxx://xxxx/xxx/xxx1',
+                                     'md5hash': """add6bb58e139be103324d04d8"""
+                                                """2d8f546"""}
+
+        resp, agent_xen = self.client.create_agent(**params_xen)
+        self.assertEqual(201, resp.status)
+
+        for expected_item, value in expected_xen.items():
+            self.assertEqual(value, agent_xen[expected_item])
+
+        params_kvm_new = expected_kvm_new = {'version': '8.0',
+                                             'url': 'xxx://xxxx/xxx/xxx2',
+                                             'md5hash': """add6bb58e139be103"""
+                                                        """324d04d82d8f547"""}
+
+        resp, resp_agent_kvm = self.client.update_agent(agent_kvm['agent_id'],
+                                                        **params_kvm_new)
+        self.assertEqual(200, resp.status)
+        for expected_item, value in expected_kvm_new.items():
+            self.assertEqual(value, resp_agent_kvm[expected_item])
+
+        resp, agents = self.client.list_agents()
+        self.assertEqual(200, resp.status)
+        self.assertTrue(len(agents) > 1)
+
+        params_filter = {'hypervisor': 'kvm'}
+        resp, agent = self.client.list_agents(params_filter)
+        self.assertEqual(200, resp.status)
+        self.assertTrue(len(agent) > 0)
+        self.assertEqual('kvm', agent[0]['hypervisor'])
+
+        resp, _ = self.client.delete_agent(agent_kvm['agent_id'])
+        self.assertEqual(204, resp.status)
+        resp, _ = self.client.delete_agent(agent_xen['agent_id'])
+        self.assertEqual(204, resp.status)
diff --git a/tempest/api/network/admin/test_l3_agent_scheduler.py b/tempest/api/network/admin/test_l3_agent_scheduler.py
index eb397ba..f4050c5 100644
--- a/tempest/api/network/admin/test_l3_agent_scheduler.py
+++ b/tempest/api/network/admin/test_l3_agent_scheduler.py
@@ -76,11 +76,8 @@
         resp, body = self.admin_client.remove_router_from_l3_agent(
             self.agent['id'], router['router']['id'])
         self.assertEqual('204', resp['status'])
-        resp, body = self.admin_client.list_l3_agents_hosting_router(
-            router['router']['id'])
-        for agent in body['agents']:
-            l3_agent_ids.append(agent['id'])
-        self.assertNotIn(self.agent['id'], l3_agent_ids)
+        # NOTE(afazekas): The deletion not asserted, because neutron
+        # is not forbidden to reschedule the router to the same agent
 
 
 class L3AgentSchedulerTestXML(L3AgentSchedulerTestJSON):
diff --git a/tempest/api/network/test_load_balancer.py b/tempest/api/network/test_load_balancer.py
index ba92199..695dbf8 100644
--- a/tempest/api/network/test_load_balancer.py
+++ b/tempest/api/network/test_load_balancer.py
@@ -131,6 +131,7 @@
         # Verification of vip delete
         resp, body = self.client.delete_vip(vip['id'])
         self.assertEqual('204', resp['status'])
+        self.client.wait_for_resource_deletion('vip', vip['id'])
         # Verification of pool update
         new_name = "New_pool"
         resp, body = self.client.update_pool(pool['id'],
diff --git a/tempest/api/orchestration/stacks/test_server_cfn_init.py b/tempest/api/orchestration/stacks/test_server_cfn_init.py
index 7e8bc2d..95deaf5 100644
--- a/tempest/api/orchestration/stacks/test_server_cfn_init.py
+++ b/tempest/api/orchestration/stacks/test_server_cfn_init.py
@@ -17,6 +17,7 @@
 from tempest.common.utils import data_utils
 from tempest.common.utils.linux import remote_client
 from tempest import config
+from tempest import exceptions
 from tempest.openstack.common import log as logging
 from tempest import test
 
@@ -66,18 +67,13 @@
               content: smoke test complete
             /etc/cfn/cfn-credentials:
               content:
-                Fn::Join:
-                - ''
-                - - AWSAccessKeyId=
-                  - {Ref: SmokeKeys}
-                  - '
-
-                    '
-                  - AWSSecretKey=
-                  - Fn::GetAtt: [SmokeKeys, SecretAccessKey]
-                  - '
-
-                    '
+                Fn::Replace:
+                - SmokeKeys: {Ref: SmokeKeys}
+                  SecretAccessKey:
+                    'Fn::GetAtt': [SmokeKeys, SecretAccessKey]
+                - |
+                  AWSAccessKeyId=SmokeKeys
+                  AWSSecretKey=SecretAccessKey
               mode: '000400'
               owner: root
               group: root
@@ -90,19 +86,13 @@
       networks:
       - uuid: {Ref: network}
       user_data:
-        Fn::Base64:
-          Fn::Join:
-          - ''
-          - - |-
-                #!/bin/bash -v
-                /opt/aws/bin/cfn-init
-            - |-
-                || error_exit ''Failed to run cfn-init''
-                /opt/aws/bin/cfn-signal -e 0 --data "`cat /tmp/smoke-status`" '
-            - {Ref: WaitHandle}
-            - '''
-
-              '
+        Fn::Replace:
+        - WaitHandle: {Ref: WaitHandle}
+        - |
+          #!/bin/bash -v
+          /opt/aws/bin/cfn-init
+          /opt/aws/bin/cfn-signal -e 0 --data "`cat /tmp/smoke-status`" \
+              "WaitHandle"
   WaitHandle:
     Type: AWS::CloudFormation::WaitConditionHandle
   WaitCondition:
@@ -172,9 +162,32 @@
         linux_client.validate_authentication()
 
     @test.attr(type='slow')
-    def test_stack_wait_condition_data(self):
-
+    def test_all_resources_created(self):
         sid = self.stack_identifier
+        self.client.wait_for_resource_status(
+            sid, 'WaitHandle', 'CREATE_COMPLETE')
+        self.client.wait_for_resource_status(
+            sid, 'SmokeSecurityGroup', 'CREATE_COMPLETE')
+        self.client.wait_for_resource_status(
+            sid, 'SmokeKeys', 'CREATE_COMPLETE')
+        self.client.wait_for_resource_status(
+            sid, 'CfnUser', 'CREATE_COMPLETE')
+        self.client.wait_for_resource_status(
+            sid, 'SmokeServer', 'CREATE_COMPLETE')
+        try:
+            self.client.wait_for_resource_status(
+                sid, 'WaitCondition', 'CREATE_COMPLETE')
+        except exceptions.TimeoutException as e:
+            # attempt to log the server console to help with debugging
+            # the cause of the server not signalling the waitcondition
+            # to heat.
+            resp, body = self.client.get_resource(sid, 'SmokeServer')
+            server_id = body['physical_resource_id']
+            LOG.debug('Console output for %s', server_id)
+            resp, output = self.servers_client.get_console_output(
+                server_id, None)
+            LOG.debug(output)
+            raise e
 
         # wait for create to complete.
         self.client.wait_for_stack_status(sid, 'CREATE_COMPLETE')
diff --git a/tempest/clients.py b/tempest/clients.py
index ab7deb0..7ebd983 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -61,6 +61,7 @@
     TenantUsagesClientJSON
 from tempest.services.compute.json.volumes_extensions_client import \
     VolumesExtensionsClientJSON
+from tempest.services.compute.v3.json.agents_client import AgentsV3ClientJSON
 from tempest.services.compute.v3.json.aggregates_client import \
     AggregatesV3ClientJSON
 from tempest.services.compute.v3.json.availability_zone_client import \
@@ -315,6 +316,7 @@
             self.services_v3_client = ServicesV3ClientJSON(
                 self.auth_provider)
             self.service_client = ServiceClientJSON(self.auth_provider)
+            self.agents_v3_client = AgentsV3ClientJSON(self.auth_provider)
             self.aggregates_v3_client = AggregatesV3ClientJSON(
                 self.auth_provider)
             self.aggregates_client = AggregatesClientJSON(
diff --git a/tempest/common/commands.py b/tempest/common/commands.py
index 6405eaa..c31a038 100644
--- a/tempest/common/commands.py
+++ b/tempest/common/commands.py
@@ -73,3 +73,7 @@
 
 def iptables_ns(ns, table):
     return ip_ns_exec(ns, "iptables -v -S -t " + table)
+
+
+def ovs_db_dump():
+    return sudo_cmd_call("ovsdb-client dump")
diff --git a/tempest/common/debug.py b/tempest/common/debug.py
index 8325d4d..6a496c2 100644
--- a/tempest/common/debug.py
+++ b/tempest/common/debug.py
@@ -38,3 +38,15 @@
         for table in ['filter', 'nat', 'mangle']:
             LOG.info('ns(%s) table(%s):\n%s', ns, table,
                      commands.iptables_ns(ns, table))
+
+
+def log_ovs_db():
+    if not CONF.debug.enable or not CONF.service_available.neutron:
+        return
+    db_dump = commands.ovs_db_dump()
+    LOG.info("OVS DB:\n" + db_dump)
+
+
+def log_net_debug():
+    log_ip_ns()
+    log_ovs_db()
diff --git a/tempest/config.py b/tempest/config.py
index 46dcbcc..212ee8a 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -366,6 +366,14 @@
                default="",
                help="Id of the public router that provides external "
                     "connectivity"),
+    cfg.IntOpt('build_timeout',
+               default=300,
+               help="Timeout in seconds to wait for network operation to "
+                    "complete."),
+    cfg.IntOpt('build_interval',
+               default=10,
+               help="Time in seconds between network operation status "
+                    "checks."),
 ]
 
 network_feature_group = cfg.OptGroup(name='network-feature-enabled',
diff --git a/tempest/scenario/test_minimum_basic.py b/tempest/scenario/test_minimum_basic.py
index 39b7760..24d2677 100644
--- a/tempest/scenario/test_minimum_basic.py
+++ b/tempest/scenario/test_minimum_basic.py
@@ -97,7 +97,7 @@
         except Exception:
             LOG.exception('ssh to server failed')
             self._log_console_output()
-            debug.log_ip_ns()
+            debug.log_net_debug()
             raise
 
     def check_partitions(self):
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index 489b271..d5ab3d3 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -172,7 +172,7 @@
         except Exception:
             LOG.exception('Tenant connectivity check failed')
             self._log_console_output(servers=self.servers.keys())
-            debug.log_ip_ns()
+            debug.log_net_debug()
             raise
 
     def _create_and_associate_floating_ips(self):
@@ -204,7 +204,7 @@
                 ex_msg += ": " + msg
             LOG.exception(ex_msg)
             self._log_console_output(servers=self.servers.keys())
-            debug.log_ip_ns()
+            debug.log_net_debug()
             raise
 
     def _disassociate_floating_ips(self):
diff --git a/tempest/scenario/test_security_groups_basic_ops.py b/tempest/scenario/test_security_groups_basic_ops.py
index d404dd1..b9ee040 100644
--- a/tempest/scenario/test_security_groups_basic_ops.py
+++ b/tempest/scenario/test_security_groups_basic_ops.py
@@ -343,7 +343,7 @@
                                                             should_succeed),
                             msg)
         except Exception:
-            debug.log_ip_ns()
+            debug.log_net_debug()
             raise
 
     def _test_in_tenant_block(self, tenant):
diff --git a/tempest/services/compute/v3/json/agents_client.py b/tempest/services/compute/v3/json/agents_client.py
new file mode 100644
index 0000000..6893af2
--- /dev/null
+++ b/tempest/services/compute/v3/json/agents_client.py
@@ -0,0 +1,52 @@
+# Copyright 2014 NEC Corporation.  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 json
+import urllib
+
+from tempest.common import rest_client
+from tempest import config
+
+CONF = config.CONF
+
+
+class AgentsV3ClientJSON(rest_client.RestClient):
+
+    def __init__(self, auth_provider):
+        super(AgentsV3ClientJSON, self).__init__(auth_provider)
+        self.service = CONF.compute.catalog_v3_type
+
+    def list_agents(self, params=None):
+        """List all agent builds."""
+        url = 'os-agents'
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+        resp, body = self.get(url)
+        return resp, self._parse_resp(body)
+
+    def create_agent(self, **kwargs):
+        """Create an agent build."""
+        post_body = json.dumps({'agent': kwargs})
+        resp, body = self.post('os-agents', post_body)
+        return resp, self._parse_resp(body)
+
+    def delete_agent(self, agent_id):
+        """Delete an existing agent build."""
+        return self.delete('os-agents/%s' % str(agent_id))
+
+    def update_agent(self, agent_id, **kwargs):
+        """Update an agent build."""
+        put_body = json.dumps({'agent': kwargs})
+        resp, body = self.put('os-agents/%s' % str(agent_id), put_body)
+        return resp, self._parse_resp(body)
diff --git a/tempest/services/network/network_client_base.py b/tempest/services/network/network_client_base.py
index f1bf548..41a7aa4 100644
--- a/tempest/services/network/network_client_base.py
+++ b/tempest/services/network/network_client_base.py
@@ -10,9 +10,11 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import time
 import urllib
 
 from tempest import config
+from tempest import exceptions
 
 CONF = config.CONF
 
@@ -54,6 +56,8 @@
         self.rest_client.service = CONF.network.catalog_type
         self.version = '2.0'
         self.uri_prefix = "v%s" % (self.version)
+        self.build_timeout = CONF.network.build_timeout
+        self.build_interval = CONF.network.build_interval
 
     def get_rest_client(self, auth_provider):
         raise NotImplementedError
@@ -189,3 +193,23 @@
         resp, body = self.post(uri, body)
         body = {'ports': self.deserialize_list(body)}
         return resp, body
+
+    def wait_for_resource_deletion(self, resource_type, id):
+        """Waits for a resource to be deleted."""
+        start_time = int(time.time())
+        while True:
+            if self.is_resource_deleted(resource_type, id):
+                return
+            if int(time.time()) - start_time >= self.build_timeout:
+                raise exceptions.TimeoutException
+            time.sleep(self.build_interval)
+
+    def is_resource_deleted(self, resource_type, id):
+        method = 'show_' + resource_type
+        try:
+            getattr(self, method)(id)
+        except AttributeError:
+            raise Exception("Unknown resource type %s " % resource_type)
+        except exceptions.NotFound:
+            return True
+        return False
diff --git a/tempest/test.py b/tempest/test.py
index d358510..804f17f 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -26,6 +26,8 @@
 import testresources
 import testtools
 
+from oslo.config import cfg
+
 from tempest import clients
 import tempest.common.generator.valid_generator as valid
 from tempest.common import isolated_creds
@@ -410,7 +412,17 @@
         """
         description = NegativeAutoTest.load_schema(description_file)
         LOG.debug(description)
-        generator = importutils.import_class(CONF.negative.test_generator)()
+
+        # NOTE(mkoderer): since this will be executed on import level the
+        # config doesn't have to be in place (e.g. for the pep8 job).
+        # In this case simply return.
+        try:
+            generator = importutils.import_class(
+                CONF.negative.test_generator)()
+        except cfg.ConfigFilesNotFoundError:
+            LOG.critical(
+                "Tempest config not found. Test scenarios aren't created")
+            return
         generator.validate_schema(description)
         schema = description.get("json-schema", None)
         resources = description.get("resources", [])
diff --git a/tempest/thirdparty/boto/test.py b/tempest/thirdparty/boto/test.py
index 10d421e..4c39f78 100644
--- a/tempest/thirdparty/boto/test.py
+++ b/tempest/thirdparty/boto/test.py
@@ -108,6 +108,9 @@
     CODE_RE = '.*'  # regexp makes sense in group match
 
     def match(self, exc):
+        """:returns: Retruns with an error string if not matches,
+               returns with None when matches.
+        """
         if not isinstance(exc, exception.BotoServerError):
             return "%r not an BotoServerError instance" % exc
         LOG.info("Status: %s , error_code: %s", exc.status, exc.error_code)
@@ -119,6 +122,7 @@
             return ("Error code (%s) does not match" +
                     "the expected re pattern \"%s\"") %\
                    (exc.error_code, self.CODE_RE)
+        return None
 
 
 class ClientError(BotoExceptionMatcher):
@@ -313,7 +317,7 @@
             except ValueError:
                 return "_GONE"
             except exception.EC2ResponseError as exc:
-                if colusure_matcher.match(exc):
+                if colusure_matcher.match(exc) is None:
                     return "_GONE"
                 else:
                     raise
@@ -449,7 +453,7 @@
                 return "_GONE"
             except exception.EC2ResponseError as exc:
                 if cls.ec2_error_code.\
-                        client.InvalidInstanceID.NotFound.match(exc):
+                        client.InvalidInstanceID.NotFound.match(exc) is None:
                     return "_GONE"
                 # NOTE(afazekas): incorrect code,
                 # but the resource must be destoreyd