Autotest: Use SSH For Communication to Moblab

Create an ssh tunnel for autoserv to communicate to moblab. MoblabHost calls
rpc_server_tracker to create a safe ssh connection to moblab when it is
created or moblab is rebooted.

Related Change:

1. Refactoring rpc_server_tracker to seperate tunnel creating process
|_setup_tracker_for_rpc| and URL/host construction functions,
|_setup_rpc| or |tunnel_connect|.

2. Add a |enable_ssh_tunnel_for_moblab| to enable/disable ssh tunnel for
moblab.

BUG=chromium:593965
TEST=Ran verify&repair, moblab_RunSuite:dummyServer, on local moblab to
verify ssh tunnel for moblab. Ran network_WiFi_SimpleConnect.wifi_check11g
to make sure that rpc_server_tracker still works.

Change-Id: I0d35209c3ba7b27cca00bbdff1a714ed6d8d40d3
Reviewed-on: https://chromium-review.googlesource.com/331792
Commit-Ready: Xixuan Wu <[email protected]>
Tested-by: Xixuan Wu <[email protected]>
Reviewed-by: Richard Barnette <[email protected]>
diff --git a/global_config.ini b/global_config.ini
index c4a142e..ec75011 100644
--- a/global_config.ini
+++ b/global_config.ini
@@ -331,6 +331,9 @@
 # Flags to enable/disable SSH connection for devserver.
 enable_ssh_connection_for_devserver: False
 
+# Flags to enable/disable SSH tunnel connection for moblab host.
+enable_ssh_tunnel_for_moblab: False
+
 [BUG_REPORTING]
 gs_domain: https://storage.cloud.google.com/
 chromeos_image_archive: chromeos-image-archive/
diff --git a/server/cros/dynamic_suite/frontend_wrappers.py b/server/cros/dynamic_suite/frontend_wrappers.py
index 491a162..807042e 100644
--- a/server/cros/dynamic_suite/frontend_wrappers.py
+++ b/server/cros/dynamic_suite/frontend_wrappers.py
@@ -56,6 +56,14 @@
         super(RetryingAFE, self).__init__(**dargs)
 
 
+    def set_timeout(self, timeout_min):
+        """Set timeout minutes for the AFE server.
+
+        @param timeout_min: The timeout minutes for AFE server.
+        """
+        self.timeout_min = timeout_min
+
+
     def run(self, call, **dargs):
         if retry_util is None:
             raise ImportError('Unable to import chromite. Please consider to '
diff --git a/server/hosts/moblab_host.py b/server/hosts/moblab_host.py
index 7f0faff..12cb311 100644
--- a/server/hosts/moblab_host.py
+++ b/server/hosts/moblab_host.py
@@ -16,6 +16,10 @@
 
 AUTOTEST_INSTALL_DIR = global_config.global_config.get_config_value(
         'SCHEDULER', 'drone_installation_directory')
+
+ENABLE_SSH_TUNNEL_FOR_MOBLAB = global_config.global_config.get_config_value(
+        'CROS', 'enable_ssh_tunnel_for_moblab', type=bool, default=False)
+
 #'/usr/local/autotest'
 SHADOW_CONFIG_PATH = '%s/shadow_config.ini' % AUTOTEST_INSTALL_DIR
 ATEST_PATH = '%s/cli/atest' % AUTOTEST_INSTALL_DIR
@@ -36,12 +40,34 @@
 DUT_VERIFY_SLEEP_SECS = 5
 DUT_VERIFY_TIMEOUT = 15 * 60
 MOBLAB_TMP_DIR = '/mnt/moblab/tmp'
+MOBLAB_PORT = 80
 
 
 class MoblabHost(cros_host.CrosHost):
     """Moblab specific host class."""
 
 
+    def _initialize_frontend_rpcs(self, timeout_min):
+        """Initialize frontends for AFE and TKO for a moblab host.
+
+        AFE and TKO are initialized differently based on |_use_tunnel|,
+        which indicates that whether to use ssh tunnel to connect to moblab.
+
+        @param timeout_min: The timeout minuties for AFE services.
+        """
+        if self._use_tunnel:
+            self.web_address = self.rpc_server_tracker.tunnel_connect(
+                    MOBLAB_PORT)
+        # Pass timeout_min to self.afe
+        self.afe = frontend_wrappers.RetryingAFE(timeout_min=timeout_min,
+                                                 user='moblab',
+                                                 server=self.web_address)
+        # Use default timeout_min of MoblabHost for self.tko
+        self.tko = frontend_wrappers.RetryingTKO(timeout_min=self.timeout_min,
+                                                 user='moblab',
+                                                 server=self.web_address)
+
+
     def _initialize(self, *args, **dargs):
         super(MoblabHost, self)._initialize(*args, **dargs)
         # Clear the Moblab Image Storage so that staging an image is properly
@@ -50,13 +76,10 @@
             self.run('rm -rf %s/*' % MOBLAB_IMAGE_STORAGE)
         self._dhcpd_leasefile = None
         self.web_address = dargs.get('web_address', self.hostname)
-        timeout_min = dargs.get('rpc_timeout_min', 1)
-        self.afe = frontend_wrappers.RetryingAFE(timeout_min=timeout_min,
-                                                 user='moblab',
-                                                 server=self.web_address)
-        self.tko = frontend_wrappers.RetryingTKO(timeout_min=timeout_min,
-                                                 user='moblab',
-                                                 server=self.web_address)
+        self._use_tunnel = (ENABLE_SSH_TUNNEL_FOR_MOBLAB and
+                            self.web_address == self.hostname)
+        self.timeout_min = dargs.get('rpc_timeout_min', 1)
+        self._initialize_frontend_rpcs(self.timeout_min)
 
 
     @staticmethod
@@ -138,12 +161,14 @@
 
         @raises urllib2.HTTPError if AFE does not respond within the timeout.
         """
-        # Use a new AFE object with a longer timeout to wait for the AFE to
-        # load.
-        afe = frontend_wrappers.RetryingAFE(timeout_min=timeout_min,
-                                            server=self.hostname)
+        # Use moblabhost's own AFE object with a longer timeout to wait for the
+        # AFE to load. Also re-create the ssh tunnel for connections to moblab.
+        # Set the timeout_min to be longer than self.timeout_min for rebooting.
+        self._initialize_frontend_rpcs(timeout_min)
         # Verify the AFE can handle a simple request.
-        afe.get_hosts()
+        self.afe.get_hosts()
+        # Reset the timeout_min after rebooting checks for afe services.
+        self.afe.set_timeout(self.timeout_min)
 
 
     def _wake_devices(self):
diff --git a/server/hosts/rpc_server_tracker.py b/server/hosts/rpc_server_tracker.py
index f771576..859ef7c 100644
--- a/server/hosts/rpc_server_tracker.py
+++ b/server/hosts/rpc_server_tracker.py
@@ -28,10 +28,10 @@
     """
 
     _RPC_PROXY_URL_FORMAT = 'http://localhost:%d'
+    _RPC_HOST_ADDRESS_FORMAT = 'localhost:%d'
     _RPC_SHUTDOWN_POLLING_PERIOD_SECONDS = 2
     _RPC_SHUTDOWN_TIMEOUT_SECONDS = 10
 
-
     def __init__(self, host):
         """
         @param port: The host object associated with this instance of
@@ -41,8 +41,8 @@
         self._rpc_proxy_map = {}
 
 
-    def _setup_rpc(self, port, command_name, remote_pid=None):
-        """Sets up a tunnel process and performs rpc connection book keeping.
+    def _setup_port(self, port, command_name, remote_pid=None):
+        """Sets up a tunnel process and register it to rpc_server_tracker.
 
         Chrome OS on the target closes down most external ports for security.
         We could open the port, but doing that would conflict with security
@@ -72,15 +72,43 @@
 
         @param port: The remote forwarding port.
         @param command_name: The name of the remote process, to terminate
-                              using pkill.
+                                using pkill.
+        @param remote_pid: The PID of the remote background process
+                            as a string.
 
-        @return A url that we can use to initiate the rpc connection.
+        @return the local port which is used for port forwarding on the ssh
+                    client.
         """
         self.disconnect(port)
         local_port = utils.get_unused_port()
         tunnel_proc = self._host.create_ssh_tunnel(port, local_port)
         self._rpc_proxy_map[port] = (command_name, tunnel_proc, remote_pid)
-        return self._RPC_PROXY_URL_FORMAT % local_port
+        return local_port
+
+
+    def _setup_rpc(self, port, command_name, remote_pid=None):
+        """Construct a URL for an rpc connection using ssh tunnel.
+
+        @param port: The remote forwarding port.
+        @param command_name: The name of the remote process, to terminate
+                              using pkill.
+        @param remote_pid: The PID of the remote background process
+                            as a string.
+
+        @return a url that we can use to initiate the rpc connection.
+        """
+        return self._RPC_PROXY_URL_FORMAT % self._setup_port(
+                port, command_name, remote_pid=remote_pid)
+
+
+    def tunnel_connect(self, port):
+        """Construct a host address using ssh tunnel.
+
+        @param port: The remote forwarding port.
+
+        @return a host address using ssh tunnel.
+        """
+        return self._RPC_HOST_ADDRESS_FORMAT % self._setup_port(port, None)
 
 
     def xmlrpc_connect(self, command, port, command_name=None,