Merge "Adds scenario for hotplug nic in network_basic_ops"
diff --git a/tempest/common/utils/linux/remote_client.py b/tempest/common/utils/linux/remote_client.py
index b833ee7..8420ad0 100644
--- a/tempest/common/utils/linux/remote_client.py
+++ b/tempest/common/utils/linux/remote_client.py
@@ -94,3 +94,18 @@
     def get_mac_address(self):
         cmd = "/sbin/ifconfig | awk '/HWaddr/ {print $5}'"
         return self.ssh_client.exec_command(cmd)
+
+    def get_ip_list(self):
+        cmd = "/bin/ip address"
+        return self.ssh_client.exec_command(cmd)
+
+    def assign_static_ip(self, nic, addr):
+        cmd = "sudo /bin/ip addr add {ip}/{mask} dev {nic}".format(
+            ip=addr, mask=CONF.network.tenant_network_mask_bits,
+            nic=nic
+        )
+        return self.ssh_client.exec_command(cmd)
+
+    def turn_nic_on(self, nic):
+        cmd = "sudo /bin/ip link set {nic} up".format(nic=nic)
+        return self.ssh_client.exec_command(cmd)
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index ca8ac3b..f06a850 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -513,7 +513,7 @@
         ports = self._list_ports(tenant_id=tenant_id)
         return len(ports)
 
-    def _create_subnet(self, network, namestart='subnet-smoke-'):
+    def _create_subnet(self, network, namestart='subnet-smoke-', **kwargs):
         """
         Create a subnet for the given network within the cidr block
         configured for tenant networks.
@@ -546,6 +546,7 @@
                     cidr=str_cidr,
                 ),
             )
+            body['subnet'].update(kwargs)
             try:
                 result = self.network_client.create_subnet(body=body)
                 break
@@ -703,6 +704,28 @@
                                                   private_key)
             linux_client.validate_authentication()
 
+    def _check_remote_connectivity(self, source, dest, should_succeed=True):
+        """
+        check ping server via source ssh connection
+
+        :param source: RemoteClient: an ssh connection from which to ping
+        :param dest: and IP to ping against
+        :param should_succeed: boolean should ping succeed or not
+        :returns: boolean -- should_succeed == ping
+        :returns: ping is false if ping failed
+        """
+        def ping_remote():
+            try:
+                source.ping_host(dest)
+            except exceptions.SSHExecCommandFailed:
+                LOG.exception('Failed to ping host via ssh connection')
+                return not should_succeed
+            return should_succeed
+
+        return tempest.test.call_until_true(ping_remote,
+                                            CONF.compute.ping_timeout,
+                                            1)
+
     def _create_security_group_nova(self, client=None,
                                     namestart='secgroup-smoke-',
                                     tenant_id=None):
diff --git a/tempest/scenario/test_network_basic_ops.py b/tempest/scenario/test_network_basic_ops.py
index 998a474..e441415 100644
--- a/tempest/scenario/test_network_basic_ops.py
+++ b/tempest/scenario/test_network_basic_ops.py
@@ -15,6 +15,9 @@
 #    under the License.
 import collections
 
+import re
+
+from tempest.api.network import common as net_common
 from tempest.common import debug
 from tempest.common.utils import data_utils
 from tempest import config
@@ -36,36 +39,6 @@
     boot VM's with Neutron-managed networking, and attempts to
     verify network connectivity as follows:
 
-     * For a freshly-booted VM with an IP address ("port") on a given network:
-
-       - the Tempest host can ping the IP address.  This implies, but
-         does not guarantee (see the ssh check that follows), that the
-         VM has been assigned the correct IP address and has
-         connectivity to the Tempest host.
-
-       - the Tempest host can perform key-based authentication to an
-         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
-
-       - associate detached floating ip to a new VM and verify connectivity.
-       VMs are created with unique keypair so connectivity also asserts that
-       floating IP is associated with the new VM instead of the old one
-
-       # TODO(mnewby) - Need to implement the following:
-       - the Tempest host can ssh into the VM via the IP address and
-         successfully execute the following:
-
-         - ping an external IP address, implying external connectivity.
-
-         - ping an external hostname, implying that dns is correctly
-           configured.
-
-         - ping an internal IP address, implying connectivity to another
-           VM on the same network.
-
      There are presumed to be two types of networks: tenant and
      public.  A tenant network may or may not be reachable from the
      Tempest host.  A public network is assumed to be reachable from
@@ -250,9 +223,125 @@
         self.floating_ip_tuple = Floating_IP_tuple(
             floating_ip, serv_dict['server'])
 
+    def _create_new_network(self):
+        self.new_net = self._create_network(self.tenant_id)
+        self.addCleanup(self.cleanup_wrapper, self.new_net)
+        self.new_subnet = self._create_subnet(
+            network=self.new_net,
+            gateway_ip=None)
+        self.addCleanup(self.cleanup_wrapper, self.new_subnet)
+
+    def _hotplug_server(self):
+        old_floating_ip, server = self.floating_ip_tuple
+        ip_address = old_floating_ip.floating_ip_address
+        private_key = self.servers[server].private_key
+        ssh_client = self.get_remote_client(ip_address,
+                                            private_key=private_key)
+        old_nic_list = self._get_server_nics(ssh_client)
+        # get a port from a list of one item
+        port_list = self._list_ports(device_id=server.id)
+        self.assertEqual(1, len(port_list))
+        old_port = port_list[0]
+        self.compute_client.servers.interface_attach(server=server,
+                                                     net_id=self.new_net.id,
+                                                     port_id=None,
+                                                     fixed_ip=None)
+        # move server to the head of the cleanup list
+        self.addCleanup(self.cleanup_wrapper, server)
+
+        def check_ports():
+            port_list = [port for port in
+                         self._list_ports(device_id=server.id)
+                         if port != old_port]
+            return len(port_list) == 1
+
+        test.call_until_true(check_ports, 60, 1)
+        new_port_list = [p for p in
+                         self._list_ports(device_id=server.id)
+                         if p != old_port]
+        self.assertEqual(1, len(new_port_list))
+        new_port = new_port_list[0]
+        new_port = net_common.DeletablePort(client=self.network_client,
+                                            **new_port)
+        new_nic_list = self._get_server_nics(ssh_client)
+        diff_list = [n for n in new_nic_list if n not in old_nic_list]
+        self.assertEqual(1, len(diff_list))
+        num, new_nic = diff_list[0]
+        ssh_client.assign_static_ip(nic=new_nic,
+                                    addr=new_port.fixed_ips[0]['ip_address'])
+        ssh_client.turn_nic_on(nic=new_nic)
+
+    def _get_server_nics(self, ssh_client):
+        reg = re.compile(r'(?P<num>\d+): (?P<nic_name>\w+):')
+        ipatxt = ssh_client.get_ip_list()
+        return reg.findall(ipatxt)
+
+    def _check_network_internal_connectivity(self):
+        """
+        via ssh check VM internal connectivity:
+        - ping internal DHCP port, implying in-tenant connectivty
+        """
+        floating_ip, server = self.floating_ip_tuple
+        # get internal ports' ips:
+        # get all network ports in the new network
+        internal_ips = (p['fixed_ips'][0]['ip_address'] for p in
+                        self._list_ports(tenant_id=server.tenant_id,
+                                         network_id=self.new_net.id)
+                        if p['device_owner'].startswith('network'))
+
+        ip_address = floating_ip.floating_ip_address
+        private_key = self.servers[server].private_key
+        ssh_source = self._ssh_to_server(ip_address, private_key)
+
+        for remote_ip in internal_ips:
+            try:
+                self.assertTrue(self._check_remote_connectivity(ssh_source,
+                                                                remote_ip),
+                                "Timed out waiting for %s to become "
+                                "reachable" % remote_ip)
+            except Exception:
+                LOG.exception("Unable to access {dest} via ssh to "
+                              "floating-ip {src}".format(dest=remote_ip,
+                                                         src=floating_ip))
+                debug.log_ip_ns()
+                raise
+
     @test.attr(type='smoke')
     @test.services('compute', 'network')
     def test_network_basic_ops(self):
+        """
+        For a freshly-booted VM with an IP address ("port") on a given
+            network:
+
+        - the Tempest host can ping the IP address.  This implies, but
+         does not guarantee (see the ssh check that follows), that the
+         VM has been assigned the correct IP address and has
+         connectivity to the Tempest host.
+
+        - the Tempest host can perform key-based authentication to an
+         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
+
+        - associate detached floating ip to a new VM and verify connectivity.
+        VMs are created with unique keypair so connectivity also asserts that
+        floating IP is associated with the new VM instead of the old one
+
+        # TODO(mnewby) - Need to implement the following:
+        - the Tempest host can ssh into the VM via the IP address and
+         successfully execute the following:
+
+         - ping an external IP address, implying external connectivity.
+
+         - ping an external hostname, implying that dns is correctly
+           configured.
+
+         - ping an internal IP address, implying connectivity to another
+           VM on the same network.
+
+        """
         self._check_public_network_connectivity(should_connect=True)
         self._disassociate_floating_ips()
         self._check_public_network_connectivity(should_connect=False,
@@ -262,3 +351,20 @@
         self._check_public_network_connectivity(should_connect=True,
                                                 msg="after re-associate "
                                                     "floating ip")
+
+    @test.attr(type='smoke')
+    @test.services('compute', 'network')
+    def test_hotplug_nic(self):
+        """
+        1. create a new network, with no gateway (to prevent overwriting VM's
+            gateway)
+        2. connect VM to new network
+        3. set static ip and bring new nic up
+        4. check VM can ping new network dhcp port
+
+        """
+
+        self._check_public_network_connectivity(should_connect=True)
+        self._create_new_network()
+        self._hotplug_server()
+        self._check_network_internal_connectivity()
diff --git a/tempest/scenario/test_security_groups_basic_ops.py b/tempest/scenario/test_security_groups_basic_ops.py
index a26e0cf..d404dd1 100644
--- a/tempest/scenario/test_security_groups_basic_ops.py
+++ b/tempest/scenario/test_security_groups_basic_ops.py
@@ -17,7 +17,6 @@
 from tempest.common import debug
 from tempest.common.utils import data_utils
 from tempest import config
-from tempest import exceptions
 from tempest.openstack.common import log as logging
 from tempest.scenario import manager
 from tempest import test
@@ -332,28 +331,6 @@
                                                private_key=private_key)
         return access_point_ssh
 
-    def _test_remote_connectivity(self, source, dest, should_succeed=True):
-        """
-        check ping server via source ssh connection
-
-        :param source: RemoteClient: an ssh connection from which to ping
-        :param dest: and IP to ping against
-        :param should_succeed: boolean should ping succeed or not
-        :returns: boolean -- should_succeed == ping
-        :returns: ping is false if ping failed
-        """
-        def ping_remote():
-            try:
-                source.ping_host(dest)
-            except exceptions.SSHExecCommandFailed as ex:
-                LOG.debug(ex)
-                return not should_succeed
-            return should_succeed
-
-        return test.call_until_true(ping_remote,
-                                    CONF.compute.ping_timeout,
-                                    1)
-
     def _check_connectivity(self, access_point, ip, should_succeed=True):
         if should_succeed:
             msg = "Timed out waiting for %s to become reachable" % ip
@@ -362,8 +339,8 @@
             return True
             msg = "%s is reachable" % ip
         try:
-            self.assertTrue(self._test_remote_connectivity(access_point, ip,
-                                                           should_succeed),
+            self.assertTrue(self._check_remote_connectivity(access_point, ip,
+                                                            should_succeed),
                             msg)
         except Exception:
             debug.log_ip_ns()