[autotest] add a --ssh_options option to autoserv

This CL follows the template of
https://chromium-review.googlesource.com/167415

BUG=chromium:285378
TEST=Unit tests pass. Manually verified that specifying
--ssh_options="-F /dev/null -i /dev/null" caused those flags to be
threaded into ssh commands.

Change-Id: Ie0496862d05d42192d55501fc0ed1c3a6274fe79
Reviewed-on: https://chromium-review.googlesource.com/168099
Reviewed-by: Aviv Keshet <[email protected]>
Tested-by: Aviv Keshet <[email protected]>
Commit-Queue: Aviv Keshet <[email protected]>
diff --git a/server/autoserv b/server/autoserv
index 95d6c3d..b3b3bb4 100755
--- a/server/autoserv
+++ b/server/autoserv
@@ -105,6 +105,7 @@
     verify_job_repo_url = parser.options.verify_job_repo_url
     skip_crash_collection = parser.options.skip_crash_collection
     ssh_verbosity = int(parser.options.ssh_verbosity)
+    ssh_options = parser.options.ssh_options
 
     # can't be both a client and a server side test
     if client and server:
@@ -158,7 +159,8 @@
     job = server_job.server_job(control, parser.args[1:], results, label,
                                 user, machines, client, parse_job,
                                 ssh_user, ssh_port, ssh_pass,
-                                ssh_verbosity_flag, test_retry, **kwargs)
+                                ssh_verbosity_flag, ssh_options,
+                                test_retry, **kwargs)
     job.logging.start_logging()
     job.init_parser()
 
diff --git a/server/autoserv_parser.py b/server/autoserv_parser.py
index f4a1af0..682a78b 100644
--- a/server/autoserv_parser.py
+++ b/server/autoserv_parser.py
@@ -161,6 +161,10 @@
                                type="choice", choices=["0", "1", "2", "3"],
                                help=("Verbosity level for ssh, between 0 "
                                      "and 3 inclusive. [default: 0]"))
+        self.parser.add_option("--ssh_options", action="store",
+                               dest="ssh_options", default=None,
+                               help=("A string giving command line flags "
+                                     "that will be included in ssh commands"))
 
 
     def parse_args(self):
diff --git a/server/autoserv_utils.py b/server/autoserv_utils.py
index 752de22..69ac3f7 100644
--- a/server/autoserv_utils.py
+++ b/server/autoserv_utils.py
@@ -14,7 +14,8 @@
                              queue_entry=None, verbose=True,
                              write_pidfile=True, fast_mode=False,
                              ssh_verbosity=0,
-                             no_console_prefix=False):
+                             no_console_prefix=False,
+                             ssh_options=None,):
     """
     Construct an autoserv command from a job or host queue entry.
 
@@ -39,6 +40,8 @@
                           verbosity level of ssh. Default: 0.
     @param no_console_prefix: If true, supress timestamps and other prefix info
                               in autoserv console logs.
+    @param ssh_options: A string giving extra arguments to be tacked on to
+                        ssh commands.
     @returns The autoserv command line as a list of executable + parameters.
     """
     command = [os.path.join(autoserv_directory, 'autoserv')]
@@ -55,6 +58,9 @@
     if ssh_verbosity:
         command += ['--ssh_verbosity', str(ssh_verbosity)]
 
+    if ssh_options:
+        command += ['--ssh_options', ssh_options]
+
     if no_console_prefix:
         command += ['--no_console_prefix']
 
diff --git a/server/hosts/factory.py b/server/hosts/factory.py
index bcc22d0..6c81618 100644
--- a/server/hosts/factory.py
+++ b/server/hosts/factory.py
@@ -1,7 +1,7 @@
 """Provides a factory method to create a host object."""
 
 
-from autotest_lib.client.common_lib import utils, error, global_config
+from autotest_lib.client.common_lib import error, global_config
 from autotest_lib.server import autotest, utils as server_utils
 from autotest_lib.server.hosts import site_factory, site_host, ssh_host, serial
 from autotest_lib.server.hosts import logfile_monitor
@@ -102,6 +102,8 @@
     hostname, args['user'], args['password'], args['port'] = \
             server_utils.parse_machine(hostname, ssh_user, ssh_pass, ssh_port)
     args['ssh_verbosity_flag'] = ssh_verbosity_flag
+    args['ssh_options'] = ssh_options
+
 
     # create a custom host class for this machine and return an instance of it
     host_class = type("%s_host" % hostname, tuple(classes), {})
diff --git a/server/hosts/site_host.py b/server/hosts/site_host.py
index 063d341..3c09b1f 100644
--- a/server/hosts/site_host.py
+++ b/server/hosts/site_host.py
@@ -208,6 +208,7 @@
 
 
     def _initialize(self, hostname, servo_args=None, ssh_verbosity_flag='',
+                    ssh_options='',
                     *args, **dargs):
         """Initialize superclasses, and |self.servo|.
 
@@ -229,6 +230,7 @@
         self.env['LIBC_FATAL_STDERR_'] = '1'
         self._rpc_proxy_map = {}
         self._ssh_verbosity_flag = ssh_verbosity_flag
+        self._ssh_options = ssh_options
         self.servo = _get_lab_servo(hostname)
         if not self.servo and servo_args is not None:
             self.servo = servo.Servo(**servo_args)
@@ -1003,12 +1005,14 @@
         @param connect_timeout Ignored.
         @param alive_interval Ignored.
         """
-        base_command = ('/usr/bin/ssh -a -x %s %s -o StrictHostKeyChecking=no'
+        base_command = ('/usr/bin/ssh -a -x %s %s %s'
+                        ' -o StrictHostKeyChecking=no'
                         ' -o UserKnownHostsFile=/dev/null -o BatchMode=yes'
                         ' -o ConnectTimeout=30 -o ServerAliveInterval=180'
                         ' -o ServerAliveCountMax=3 -o ConnectionAttempts=4'
                         ' -o Protocol=2 -l %s -p %d')
-        return base_command % (self._ssh_verbosity_flag, opts, user, port)
+        return base_command % (self._ssh_verbosity_flag, self._ssh_options,
+                               opts, user, port)
 
 
     def _create_ssh_tunnel(self, port, local_port):
diff --git a/server/server_job.py b/server/server_job.py
index 225820a..f113b23 100644
--- a/server/server_job.py
+++ b/server/server_job.py
@@ -141,10 +141,12 @@
 
     _STATUS_VERSION = 1
 
+    # TODO crbug.com/285395 eliminate ssh_verbosity_flag
     def __init__(self, control, args, resultdir, label, user, machines,
                  client=False, parse_job='',
                  ssh_user='root', ssh_port=22, ssh_pass='',
-                 ssh_verbosity_flag='', test_retry=0, group_name='',
+                 ssh_verbosity_flag='', ssh_options='',
+                 test_retry=0, group_name='',
                  tag='', disable_sysinfo=False,
                  control_filename=SERVER_CONTROL_FILENAME):
         """
@@ -163,6 +165,8 @@
         @param ssh_pass: The SSH passphrase, if needed.
         @param ssh_verbosity_flag: The SSH verbosity flag, '-v', '-vv',
                 '-vvv', or an empty string if not needed.
+        @param ssh_options: A string giving additional options that will be
+                            included in ssh commands.
         @param test_retry: The number of times to retry a test if the test did
                 not complete successfully.
         @param group_name: If supplied, this will be written out as
@@ -199,6 +203,7 @@
         self._ssh_port = ssh_port
         self._ssh_pass = ssh_pass
         self._ssh_verbosity_flag = ssh_verbosity_flag
+        self._ssh_options = ssh_options
         self.tag = tag
         self.last_boot_tag = None
         self.hosts = set()
@@ -346,6 +351,23 @@
             self.__insert_test(test)
         self._using_parser = False
 
+    # TODO crbug.com/285395 add a kwargs parameter.
+    def _make_namespace(self):
+        """Create a namespace dictionary to be passed along to control file.
+
+        Creates a namespace argument populated with standard values:
+        machines, job, ssh_user, ssh_port, ssh_pass, ssh_verbosity_flag,
+        and ssh_options.
+        """
+        namespace = {'machines' : self.machines,
+                     'job' : self,
+                     'ssh_user' : self._ssh_user,
+                     'ssh_port' : self._ssh_port,
+                     'ssh_pass' : self._ssh_pass,
+                     'ssh_verbosity_flag' : self._ssh_verbosity_flag,
+                     'ssh_options' : self._ssh_options}
+        return namespace
+
 
     def verify(self):
         """Verify machines are all ssh-able."""
@@ -354,11 +376,7 @@
         if self.resultdir:
             os.chdir(self.resultdir)
         try:
-            namespace = {'machines' : self.machines, 'job' : self,
-                         'ssh_user' : self._ssh_user,
-                         'ssh_port' : self._ssh_port,
-                         'ssh_pass' : self._ssh_pass,
-                         'ssh_verbosity_flag' : self._ssh_verbosity_flag}
+            namespace = self._make_namespace()
             self._execute_code(VERIFY_CONTROL_FILE, namespace, protect=False)
         except Exception, e:
             msg = ('Verify failed\n' + str(e) + '\n' + traceback.format_exc())
@@ -374,11 +392,7 @@
             os.chdir(self.resultdir)
 
         try:
-            namespace = {'machines' : self.machines, 'job' : self,
-                         'ssh_user' : self._ssh_user,
-                         'ssh_port' : self._ssh_port,
-                         'ssh_pass' : self._ssh_pass,
-                         'ssh_verbosity_flag' : self._ssh_verbosity_flag}
+            namespace = self._make_namespace()
             self._execute_code(RESET_CONTROL_FILE, namespace, protect=False)
         except Exception as e:
             msg = ('Reset failed\n' + str(e) + '\n' +
@@ -392,11 +406,9 @@
             raise error.AutoservError('No machines specified to repair')
         if self.resultdir:
             os.chdir(self.resultdir)
-        namespace = {'machines': self.machines, 'job': self,
-                     'ssh_user': self._ssh_user, 'ssh_port': self._ssh_port,
-                     'ssh_pass': self._ssh_pass,
-                     'ssh_verbosity_flag' : self._ssh_verbosity_flag,
-                     'protection_level': host_protection}
+
+        namespace = self._make_namespace()
+        namespace.update({'protection_level' : host_protection})
 
         self._execute_code(REPAIR_CONTROL_FILE, namespace, protect=False)
 
@@ -548,13 +560,8 @@
             control_file_dir = self.resultdir
 
         self.aborted = False
-        namespace['machines'] = machines
-        namespace['args'] = self.args
-        namespace['job'] = self
-        namespace['ssh_user'] = self._ssh_user
-        namespace['ssh_port'] = self._ssh_port
-        namespace['ssh_pass'] = self._ssh_pass
-        namespace['ssh_verbosity_flag'] = self._ssh_verbosity_flag
+        namespace.update(self._make_namespace())
+        namespace.update({'args' : self.args})
         test_start_time = int(time.time())
 
         if self.resultdir:
@@ -1067,6 +1074,7 @@
         namespace['hosts'].factory.ssh_pass = self._ssh_pass
         namespace['hosts'].factory.ssh_verbosity_flag = (
                 self._ssh_verbosity_flag)
+        namespace['hosts'].factory.ssh_options = self._ssh_options
 
 
     def _execute_code(self, code_file, namespace, protect=True):