Merge "Test for the agent management extension API"
diff --git a/.testr.conf b/.testr.conf
index 05b12c4..c25ebec 100644
--- a/.testr.conf
+++ b/.testr.conf
@@ -2,7 +2,8 @@
 test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
              OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
              OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-500} \
-             ${PYTHON:-python} -m subunit.run discover -t ./ ./tempest $LISTOPT $IDOPTION
+             OS_TEST_PATH=${OS_TEST_PATH:-./tempest} \
+             ${PYTHON:-python} -m subunit.run discover -t ./ $OS_TEST_PATH $LISTOPT $IDOPTION
 test_id_option=--load-list $IDFILE
 test_list_option=--list
 group_regex=([^\.]*\.)*
diff --git a/tempest/api/compute/images/test_images_oneserver_negative.py b/tempest/api/compute/images/test_images_oneserver_negative.py
index 864f445..2d27b81 100644
--- a/tempest/api/compute/images/test_images_oneserver_negative.py
+++ b/tempest/api/compute/images/test_images_oneserver_negative.py
@@ -49,7 +49,10 @@
             LOG.exception(exc)
             # Rebuild server if cannot reach the ACTIVE state
             # Usually it means the server had a serius accident
-            self.__class__.server_id = self.rebuild_server(self.server_id)
+            self._reset_server()
+
+    def _reset_server(self):
+        self.__class__.server_id = self.rebuild_server(self.server_id)
 
     @classmethod
     def setUpClass(cls):
@@ -116,6 +119,7 @@
         self.assertEqual(202, resp.status)
         image_id = data_utils.parse_image_id(resp['location'])
         self.image_ids.append(image_id)
+        self.addCleanup(self._reset_server)
 
         # Create second snapshot
         alt_snapshot_name = data_utils.rand_name('test-snap-')
@@ -139,6 +143,7 @@
         self.assertEqual(202, resp.status)
         image_id = data_utils.parse_image_id(resp['location'])
         self.image_ids.append(image_id)
+        self.addCleanup(self._reset_server)
 
         # Do not wait, attempt to delete the image, ensure it's successful
         resp, body = self.client.delete_image(image_id)
diff --git a/tempest/api/network/common.py b/tempest/api/network/common.py
index ab19fa8..528a204 100644
--- a/tempest/api/network/common.py
+++ b/tempest/api/network/common.py
@@ -47,6 +47,9 @@
     def delete(self):
         raise NotImplemented()
 
+    def __hash__(self):
+        return id(self)
+
 
 class DeletableNetwork(DeletableResource):
 
@@ -86,6 +89,23 @@
 
 class DeletableFloatingIp(DeletableResource):
 
+    def update(self, *args, **kwargs):
+        result = self.client.update_floatingip(floatingip=self.id,
+                                               body=dict(
+                                                   floatingip=dict(*args,
+                                                                   **kwargs)
+                                               ))
+        super(DeletableFloatingIp, self).update(**result['floatingip'])
+
+    def __repr__(self):
+        return '<%s addr="%s">' % (self.__class__.__name__,
+                                   self.floating_ip_address)
+
+    def __str__(self):
+        return '<"FloatingIP" addr="%s" id="%s">' % (self.__class__.__name__,
+                                                     self.floating_ip_address,
+                                                     self.id)
+
     def delete(self):
         self.client.delete_floatingip(self.id)
 
diff --git a/tempest/cli/simple_read_only/test_neutron.py b/tempest/cli/simple_read_only/test_neutron.py
index 047b17d..80376ab 100644
--- a/tempest/cli/simple_read_only/test_neutron.py
+++ b/tempest/cli/simple_read_only/test_neutron.py
@@ -44,35 +44,43 @@
             raise cls.skipException(msg)
         super(SimpleReadOnlyNeutronClientTest, cls).setUpClass()
 
+    @test.attr(type='smoke')
     def test_neutron_fake_action(self):
         self.assertRaises(subprocess.CalledProcessError,
                           self.neutron,
                           'this-does-not-exist')
 
+    @test.attr(type='smoke')
     def test_neutron_net_list(self):
         self.neutron('net-list')
 
+    @test.attr(type='smoke')
     def test_neutron_ext_list(self):
         ext = self.parser.listing(self.neutron('ext-list'))
         self.assertTableStruct(ext, ['alias', 'name'])
 
+    @test.attr(type='smoke')
     def test_neutron_dhcp_agent_list_hosting_net(self):
         self.neutron('dhcp-agent-list-hosting-net',
                      params=CONF.compute.fixed_network_name)
 
+    @test.attr(type='smoke')
     def test_neutron_agent_list(self):
         agents = self.parser.listing(self.neutron('agent-list'))
         field_names = ['id', 'agent_type', 'host', 'alive', 'admin_state_up']
         self.assertTableStruct(agents, field_names)
 
+    @test.attr(type='smoke')
     def test_neutron_floatingip_list(self):
         self.neutron('floatingip-list')
 
     @test.skip_because(bug="1240694")
+    @test.attr(type='smoke')
     def test_neutron_meter_label_list(self):
         self.neutron('meter-label-list')
 
     @test.skip_because(bug="1240694")
+    @test.attr(type='smoke')
     def test_neutron_meter_label_rule_list(self):
         self.neutron('meter-label-rule-list')
 
@@ -83,40 +91,52 @@
             if '404 Not Found' not in e.stderr:
                 self.fail('%s: Unexpected failure.' % command)
 
+    @test.attr(type='smoke')
     def test_neutron_lb_healthmonitor_list(self):
         self._test_neutron_lbaas_command('lb-healthmonitor-list')
 
+    @test.attr(type='smoke')
     def test_neutron_lb_member_list(self):
         self._test_neutron_lbaas_command('lb-member-list')
 
+    @test.attr(type='smoke')
     def test_neutron_lb_pool_list(self):
         self._test_neutron_lbaas_command('lb-pool-list')
 
+    @test.attr(type='smoke')
     def test_neutron_lb_vip_list(self):
         self._test_neutron_lbaas_command('lb-vip-list')
 
+    @test.attr(type='smoke')
     def test_neutron_net_external_list(self):
         self.neutron('net-external-list')
 
+    @test.attr(type='smoke')
     def test_neutron_port_list(self):
         self.neutron('port-list')
 
+    @test.attr(type='smoke')
     def test_neutron_quota_list(self):
         self.neutron('quota-list')
 
+    @test.attr(type='smoke')
     def test_neutron_router_list(self):
         self.neutron('router-list')
 
+    @test.attr(type='smoke')
     def test_neutron_security_group_list(self):
         security_grp = self.parser.listing(self.neutron('security-group-list'))
         self.assertTableStruct(security_grp, ['id', 'name', 'description'])
 
+    @test.attr(type='smoke')
     def test_neutron_security_group_rule_list(self):
         self.neutron('security-group-rule-list')
 
+    @test.attr(type='smoke')
     def test_neutron_subnet_list(self):
         self.neutron('subnet-list')
 
+    @test.attr(type='smoke')
     def test_neutron_help(self):
         help_text = self.neutron('help')
         lines = help_text.split('\n')
@@ -136,11 +156,14 @@
 
      # Optional arguments:
 
+    @test.attr(type='smoke')
     def test_neutron_version(self):
         self.neutron('', flags='--version')
 
+    @test.attr(type='smoke')
     def test_neutron_debug_net_list(self):
         self.neutron('net-list', flags='--debug')
 
+    @test.attr(type='smoke')
     def test_neutron_quiet_net_list(self):
         self.neutron('net-list', flags='--quiet')
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index fa46cd7..e839d20 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -628,7 +628,15 @@
         self.set_resource(data_utils.rand_name('floatingip-'), floating_ip)
         return floating_ip
 
-    def _ping_ip_address(self, ip_address):
+    def _disassociate_floating_ip(self, floating_ip):
+        """
+        :param floating_ip: type DeletableFloatingIp
+        """
+        floating_ip.update(port_id=None)
+        self.assertEqual(None, floating_ip.port_id)
+        return floating_ip
+
+    def _ping_ip_address(self, ip_address, should_succeed=True):
         cmd = ['ping', '-c1', '-w1', ip_address]
 
         def ping():
@@ -636,8 +644,7 @@
                                     stdout=subprocess.PIPE,
                                     stderr=subprocess.PIPE)
             proc.wait()
-            if proc.returncode == 0:
-                return True
+            return (proc.returncode == 0) == should_succeed
 
         return tempest.test.call_until_true(
             ping, self.config.compute.ping_timeout, 1)
@@ -649,17 +656,37 @@
                                 timeout=timeout)
         return ssh_client.test_connection_auth()
 
-    def _check_vm_connectivity(self, ip_address, username, private_key):
-        self.assertTrue(self._ping_ip_address(ip_address),
-                        "Timed out waiting for %s to become "
-                        "reachable" % ip_address)
-        self.assertTrue(self._is_reachable_via_ssh(
-            ip_address,
-            username,
-            private_key,
-            timeout=self.config.compute.ssh_timeout),
-            'Auth failure in connecting to %s@%s via ssh' %
-            (username, ip_address))
+    def _check_vm_connectivity(self, ip_address,
+                               username=None,
+                               private_key=None,
+                               should_connect=True):
+        """
+        :param ip_address: server to test against
+        :param username: server's ssh username
+        :param private_key: server's ssh private key to be used
+        :param should_connect: True/False indicates positive/negative test
+            positive - attempt ping and ssh
+            negative - attempt ping and fail if succeed
+
+        :raises: AssertError if the result of the connectivity check does
+            not match the value of the should_connect param
+        """
+        if should_connect:
+            msg = "Timed out waiting for %s to become reachable" % ip_address
+        else:
+            msg = "ip address %s is reachable" % ip_address
+        self.assertTrue(self._ping_ip_address(ip_address,
+                                              should_succeed=should_connect),
+                        msg=msg)
+        if should_connect:
+            # no need to check ssh for negative connectivity
+            self.assertTrue(self._is_reachable_via_ssh(
+                ip_address,
+                username,
+                private_key,
+                timeout=self.config.compute.ssh_timeout),
+                'Auth failure in connecting to %s@%s via ssh' %
+                (username, ip_address))
 
     def _create_security_group_nova(self, client=None,
                                     namestart='secgroup-smoke-',
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index 1418b75..d605dff 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -41,10 +41,7 @@
 
     def __init__(self, compute_client, floating_ip_map):
         self.compute_client = compute_client
-        self.unchecked = {}
-        for k in floating_ip_map.keys():
-            self.unchecked[k] = [f.floating_ip_address
-                                 for f in floating_ip_map[k]]
+        self.unchecked = floating_ip_map.copy()
 
     def run_checks(self):
         """Check for any remaining unverified floating IPs
@@ -56,16 +53,14 @@
         """
         to_delete = []
         loggable_map = {}
-        for k, check_addrs in self.unchecked.iteritems():
-            serverdata = self.compute_client.servers.get(k.id)
-            for net_name, ip_addr in serverdata.networks.iteritems():
-                for addr in ip_addr:
-                    if addr in check_addrs:
-                        check_addrs.remove(addr)
-            if len(check_addrs) == 0:
-                to_delete.append(k)
+        for check_addr, server in self.unchecked.iteritems():
+            serverdata = self.compute_client.servers.get(server.id)
+            ip_addr = [addr for sublist in serverdata.networks.values() for
+                       addr in sublist]
+            if check_addr.floating_ip_address in ip_addr:
+                to_delete.append(check_addr)
             else:
-                loggable_map[k.id] = check_addrs
+                loggable_map[server.id] = check_addr
 
         for to_del in to_delete:
             del self.unchecked[to_del]
@@ -93,6 +88,9 @@
          ssh server hosted at the IP address.  This check guarantees
          that the IP address is associated with the target VM.
 
+       - detach the floating-ip from the VM and verify that it becomes
+       unreachable
+
        # TODO(mnewby) - Need to implement the following:
        - the Tempest host can ssh into the VM via the IP address and
          successfully execute the following:
@@ -297,30 +295,34 @@
             "Timed out while waiting for the floating IP assignments "
             "to propagate")
 
-    def _assign_floating_ips(self):
+    def _create_and_associate_floating_ips(self):
         public_network_id = self.config.network.public_network_id
         for server in self.servers:
             floating_ip = self._create_floating_ip(server, public_network_id)
-            self.floating_ips.setdefault(server, [])
-            self.floating_ips[server].append(floating_ip)
+            self.floating_ips[floating_ip] = server
 
-    def _check_public_network_connectivity(self):
+    def _check_public_network_connectivity(self, should_connect=True):
         # The target login is assumed to have been configured for
         # key-based authentication by cloud-init.
         ssh_login = self.config.compute.image_ssh_user
         private_key = self.keypairs[self.tenant_id].private_key
         try:
-            for server, floating_ips in self.floating_ips.iteritems():
-                for floating_ip in floating_ips:
-                    ip_address = floating_ip.floating_ip_address
-                    self._check_vm_connectivity(ip_address,
-                                                ssh_login,
-                                                private_key)
+            for floating_ip, server in self.floating_ips.iteritems():
+                ip_address = floating_ip.floating_ip_address
+                self._check_vm_connectivity(ip_address,
+                                            ssh_login,
+                                            private_key,
+                                            should_connect=should_connect)
         except Exception as exc:
             LOG.exception(exc)
             debug.log_ip_ns()
             raise exc
 
+    def _disassociate_floating_ips(self):
+        for floating_ip, server in self.floating_ips.iteritems():
+            self._disassociate_floating_ip(floating_ip)
+            self.floating_ips[floating_ip] = None
+
     @attr(type='smoke')
     @services('compute', 'network')
     def test_network_basic_ops(self):
@@ -329,7 +331,9 @@
         self._create_networks()
         self._check_networks()
         self._create_servers()
-        self._assign_floating_ips()
+        self._create_and_associate_floating_ips()
         self._wait_for_floating_ip_association()
         self._check_tenant_network_connectivity()
-        self._check_public_network_connectivity()
+        self._check_public_network_connectivity(should_connect=True)
+        self._disassociate_floating_ips()
+        self._check_public_network_connectivity(should_connect=False)
diff --git a/tox.ini b/tox.ini
index 1d7e1b7..c7f92ae 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,12 +12,15 @@
 install_command = pip install -U {opts} {packages}
 
 [testenv:py26]
+setenv = OS_TEST_PATH=./tempest/tests
 commands = python setup.py test --slowest --testr-arg='tempest\.tests {posargs}'
 
 [testenv:py33]
+setenv = OS_TEST_PATH=./tempest/tests
 commands = python setup.py test --slowest --testr-arg='tempest\.tests {posargs}'
 
 [testenv:py27]
+setenv = OS_TEST_PATH=./tempest/tests
 commands = python setup.py test --slowest --testr-arg='tempest\.tests {posargs}'
 
 [testenv:all]