Reland "[PY3][Autotest] First go at setting test_that flag for py3"

This is a reland of 39fc2292aaeeafff91add2465bf5f6edc7299f38

Original change's description:
> [PY3][Autotest] First go at setting test_that flag for py3
>
> NOTE: its gaurded with a force back to py2 for now. But this is roughly
> what I think I am going for. There is a bit of debate I am having still
> about turning on the "no restart" flag going forward. IMO the launching
> script, (autoserv, test that, or other bins) should be able to control
> the version they want to run in going forward. 1 pager about this likely
> coming
>
> BUG=chromium:990593
> TEST=test_that with the py_version flag in both 2/3 and verfied tests
> are in py2, as well as without the flag
>
> Change-Id: If34d6866061f93e72728a3e49c02e2faf73ae628
> Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/2638720
> Reviewed-by: Greg Edelston <gredelston@google.com>
> Commit-Queue: Derek Beckett <dbeckett@chromium.org>
> Tested-by: Derek Beckett <dbeckett@chromium.org>

Bug: chromium:990593
Change-Id: I498b734e00bb57bc6ec0e38ff686583544444b61
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/2690764
Reviewed-by: Greg Edelston <gredelston@google.com>
Commit-Queue: Derek Beckett <dbeckett@chromium.org>
Tested-by: Derek Beckett <dbeckett@chromium.org>
(cherry picked from commit 80b7f446e66e74a2dbba01ef53b6c074bac0a1ca)
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/2715647
Reviewed-by: C Shapiro <shapiroc@chromium.org>
Auto-Submit: Derek Beckett <dbeckett@chromium.org>
diff --git a/client/common_lib/check_version.py b/client/common_lib/check_version.py
index 68ee16c..7600f66 100644
--- a/client/common_lib/check_version.py
+++ b/client/common_lib/check_version.py
@@ -1,19 +1,28 @@
 # This file must use Python 1.5 syntax.
 import glob
+import logging
 import os
 import sys
 
+PY_GLOBS = {
+        3: ['/usr/bin/python3*', '/usr/local/bin/python3*'],
+        2: ['/usr/bin/python2*', '/usr/local/bin/python2*']
+}
+
 
 class check_python_version:
 
-    def __init__(self):
+    def __init__(self, desired_version=2):
         # In order to ease the migration to Python3, disable the restart logic
         # when AUTOTEST_NO_RESTART is set. This makes it possible to run
         # autotest locally as Python3 before any other environment is switched
         # to Python3.
         if os.getenv("AUTOTEST_NO_RESTART"):
             return
-
+        self.desired_version = desired_version
+        if self.desired_version == 3:
+            logging.warning("Python3 not not ready yet. Swapping to Python 2.")
+            self.desired_version = 2
         # The change to prefer 2.4 really messes up any systems which have both
         # the new and old version of Python, but where the newer is default.
         # This is because packages, libraries, etc are all installed into the
@@ -23,26 +32,22 @@
         # runs) 'import common' it restarts my shell. Overall, the change was
         # fairly annoying for me (and I can't get around having 2.4 and 2.5
         # installed with 2.5 being default).
-        if sys.version_info.major >= 3:
+        if sys.version_info.major != self.desired_version:
             try:
                 # We can't restart when running under mod_python.
                 from mod_python import apache
             except ImportError:
                 self.restart()
 
-
-    PYTHON_BIN_GLOB_STRINGS = ['/usr/bin/python2*', '/usr/local/bin/python2*']
-
-
     def find_desired_python(self):
         """Returns the path of the desired python interpreter."""
         # CrOS only ever has Python 2.7 available, so pick whatever matches.
+        pyv_strings = PY_GLOBS[self.desired_version]
         pythons = []
-        for glob_str in self.PYTHON_BIN_GLOB_STRINGS:
+        for glob_str in pyv_strings:
             pythons.extend(glob.glob(glob_str))
         return pythons[0]
 
-
     def restart(self):
         python = self.find_desired_python()
         sys.stderr.write('NOTE: %s switching to %s\n' %
diff --git a/client/setup_modules.py b/client/setup_modules.py
index 9cb59eb..82e0924 100644
--- a/client/setup_modules.py
+++ b/client/setup_modules.py
@@ -1,14 +1,71 @@
-__author__ = "jadmanski@google.com (John Admanski)"
-
-import os, sys
+import os
+import re
+import sys
 
 # This must run on Python versions less than 2.4.
 dirname = os.path.dirname(sys.modules[__name__].__file__)
-common_dir = os.path.abspath(os.path.join(dirname, "common_lib"))
+common_dir = os.path.abspath(os.path.join(dirname, 'common_lib'))
 sys.path.insert(0, common_dir)
 import check_version
 sys.path.pop(0)
-check_version.check_python_version()
+
+
+def _get_pyversion_from_args():
+    """Extract, format, & pop the current py_version from args, if provided."""
+    py_version = 2
+    py_version_re = re.compile(r'--py_version=(\w+)\b')
+
+    version_found = False
+    for i, arg in enumerate(sys.argv):
+        if not arg.startswith('--py_version'):
+            continue
+        result = py_version_re.search(arg)
+        if result:
+            if version_found:
+                raise ValueError('--py_version may only be specified once.')
+            py_version = result.group(1)
+            version_found = True
+            if py_version not in ('2', '3'):
+                raise ValueError('Python version must be "2" or "3".')
+
+            # Remove the arg so other argparsers don't get grumpy.
+            sys.argv.pop(i)
+
+    return py_version
+
+
+def _desired_version():
+    """
+    Returns desired python version.
+
+    If the PY_VERSION env var is set, just return that. This is the case
+    when autoserv kicks of autotest on the server side via a job.run(), or
+    a process created a subprocess.
+
+    Otherwise, parse & pop the sys.argv for the '--py_version' flag. If no
+    flag is set, default to python 2 (for now).
+
+    """
+    # Even if the arg is in the env vars, we will attempt to get it from the
+    # args, so that it can be popped prior to other argparsers hitting.
+    py_version = _get_pyversion_from_args()
+
+    if os.getenv('PY_VERSION'):
+        return int(os.getenv('PY_VERSION'))
+
+    os.environ['PY_VERSION'] = str(py_version)
+    return int(py_version)
+
+
+desired_version = _desired_version()
+if desired_version == sys.version_info.major:
+    os.environ['AUTOTEST_NO_RESTART'] = 'True'
+else:
+    # There are cases were this can be set (ie by test_that), but a subprocess
+    # is launched in the incorrect version.
+    if os.getenv('AUTOTEST_NO_RESTART'):
+        del os.environ['AUTOTEST_NO_RESTART']
+    check_version.check_python_version(desired_version)
 
 import glob, traceback, types
 
@@ -22,7 +79,7 @@
 
 def _create_module_and_parents(name):
     """Create a module, and all the necessary parents"""
-    parts = name.split(".")
+    parts = name.split('.')
     # first create the top-level module
     parent = _create_module(parts[0])
     created_parts = [parts[0]]
@@ -33,7 +90,7 @@
         module = types.ModuleType(child_name)
         setattr(parent, child_name, module)
         created_parts.append(child_name)
-        sys.modules[".".join(created_parts)] = module
+        sys.modules['.'.join(created_parts)] = module
         parent = module
 
 
@@ -45,11 +102,11 @@
         full_name = os.path.join(path, filename)
         if not os.path.isdir(full_name):
             continue   # skip files
-        if "." in filename:
-            continue   # if "." is in the name it's not a valid package name
+        if '.' in filename:
+            continue  # if '.' is in the name it's not a valid package name
         if not os.access(full_name, os.R_OK | os.X_OK):
             continue   # need read + exec access to make a dir importable
-        if "__init__.py" in os.listdir(full_name):
+        if '__init__.py' in os.listdir(full_name):
             names.append(filename)
     # import all the packages and insert them into 'parent_module'
     sys.path.insert(0, path)
@@ -58,7 +115,7 @@
         # add the package to the parent
         parent_module = sys.modules[parent_module_name]
         setattr(parent_module, name, module)
-        full_name = parent_module_name + "." + name
+        full_name = parent_module_name + '.' + name
         sys.modules[full_name] = module
     # restore the system path
     sys.path.pop(0)
@@ -91,10 +148,10 @@
 
 def _monkeypatch_logging_handle_error():
     # Hack out logging.py*
-    logging_py = os.path.join(os.path.dirname(__file__), "common_lib",
-                              "logging.py*")
+    logging_py = os.path.join(os.path.dirname(__file__), 'common_lib',
+                              'logging.py*')
     if glob.glob(logging_py):
-        os.system("rm -f %s" % logging_py)
+        os.system('rm -f %s' % logging_py)
 
     # Monkey patch our own handleError into the logging module's StreamHandler.
     # A nicer way of doing this -might- be to have our own logging module define
@@ -134,6 +191,6 @@
         # running as a client.
         # This is primarily for the benefit of frontend and tko so that they
         # may use libraries other than those available as system packages.
-        sys.path.insert(0, os.path.join(base_path, "site-packages"))
+        sys.path.insert(0, os.path.join(base_path, 'site-packages'))
 
     _monkeypatch_logging_handle_error()
diff --git a/server/autoserv_parser.py b/server/autoserv_parser.py
index 6733d70..0216100 100644
--- a/server/autoserv_parser.py
+++ b/server/autoserv_parser.py
@@ -238,6 +238,12 @@
                      ' enabled. The default value is provided via the global'
                      ' config setting for AUTOSERV/container_base_name.'
         )
+        self.parser.add_argument('--py_version',
+                                 action='store',
+                                 dest='py_version',
+                                 default='2',
+                                 type=str,
+                                 choices=['2', '3'])
 
         #
         # Warning! Please read before adding any new arguments!
diff --git a/server/autoserv_utils.py b/server/autoserv_utils.py
index 76e8601..f609fdc 100644
--- a/server/autoserv_utils.py
+++ b/server/autoserv_utils.py
@@ -71,7 +71,16 @@
 
     """
     script_name = 'virtualenv_autoserv' if use_virtualenv else 'autoserv'
-    command = [os.path.join(autoserv_directory, script_name)]
+
+    full_script_path = os.path.join(autoserv_directory, script_name)
+
+    # virtualenv_autoserv is a `POSIX shell script, ASCII text executable`.
+    # Calling with `sys.executable` would fail because python doesn't
+    # interpret shebangs itself.
+    if use_virtualenv:
+        command = [full_script_path]
+    else:
+        command = [sys.executable, full_script_path]
 
     if write_pidfile:
         command.append('-p')
@@ -131,4 +140,8 @@
     if in_lab:
         command.extend(['--lab', 'True'])
 
+    py_version = os.getenv('PY_VERSION')
+    if py_version:
+        command.extend(['--py_version', py_version])
+
     return command + extra_args
diff --git a/server/autotest.py b/server/autotest.py
index d195bd0..7a35fba 100644
--- a/server/autotest.py
+++ b/server/autotest.py
@@ -55,6 +55,11 @@
 LOG_BUFFER_SIZE_BYTES = 64
 
 
+def _set_py_version():
+    """Return the py_version flag obtained from the set environmental var."""
+    return '--py_version=%s' % int(os.getenv('PY_VERSION'))
+
+
 class AutodirNotFoundError(Exception):
     """No Autotest installation could be found."""
 
@@ -735,23 +740,36 @@
 
 
     def get_background_cmd(self, section):
-        cmd = ['nohup', os.path.join(self.autodir, 'bin/autotest_client')]
+        cmd = [
+                'nohup',
+                os.path.join(self.autodir, 'bin/autotest_client'),
+                _set_py_version()
+        ]
         cmd += self.get_base_cmd_args(section)
         cmd += ['>/dev/null', '2>/dev/null', '&']
         return ' '.join(cmd)
 
 
     def get_daemon_cmd(self, section, monitor_dir):
-        cmd = ['nohup', os.path.join(self.autodir, 'bin/autotestd'),
-               monitor_dir, '-H autoserv']
+        cmd = [
+                'nohup',
+                os.path.join(self.autodir, 'bin/autotestd'), monitor_dir,
+                '-H autoserv',
+                _set_py_version()
+        ]
         cmd += self.get_base_cmd_args(section)
         cmd += ['>/dev/null', '2>/dev/null', '&']
         return ' '.join(cmd)
 
 
     def get_monitor_cmd(self, monitor_dir, stdout_read, stderr_read):
-        cmd = [os.path.join(self.autodir, 'bin', 'autotestd_monitor'),
-               monitor_dir, str(stdout_read), str(stderr_read)]
+        cmd = [
+                os.path.join(self.autodir, 'bin', 'autotestd_monitor'),
+                monitor_dir,
+                str(stdout_read),
+                str(stderr_read),
+                _set_py_version()
+        ]
         return ' '.join(cmd)
 
 
@@ -1419,8 +1437,8 @@
             server_package = os.path.join(self.job.pkgmgr.pkgmgr_dir,
                                           'packages', pkg_name)
             if os.path.exists(server_package):
-              self.host.send_file(server_package, remote_dest)
-              return
+                self.host.send_file(server_package, remote_dest)
+                return
 
         except error.AutoservRunError:
             msg = ("Package %s could not be sent from the package cache." %