Merge "scenario/network_basic_ops: detach floating-ip"
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/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)