Build API: Enter chroot on INSIDE assertion.

BUG=chromium:953049
TEST=run_tests
TEST=manual - called INSIDE endpoint and verified entry
Cq-Depend: chromium:1569042

Change-Id: I78ad1a249d90b5de112ed6da4db319bcf19ff836
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/1570352
Tested-by: Alex Klein <saklein@chromium.org>
Reviewed-by: Evan Hernandez <evanhernandez@chromium.org>
Reviewed-by: Sean Abraham <seanabraham@chromium.org>
diff --git a/scripts/build_api.py b/scripts/build_api.py
index 785a25c..99cf69b 100644
--- a/scripts/build_api.py
+++ b/scripts/build_api.py
@@ -9,6 +9,7 @@
 
 import importlib
 import os
+import shutil
 
 from google.protobuf import json_format
 from google.protobuf import symbol_database
@@ -22,6 +23,8 @@
 from chromite.api.gen.chromite.api import sdk_pb2
 from chromite.api.gen.chromite.api import sysroot_pb2
 from chromite.api.gen.chromite.api import test_pb2
+from chromite.api.gen.chromiumos import common_pb2
+from chromite.lib import constants
 from chromite.lib import commandline
 from chromite.lib import cros_build_lib
 from chromite.lib import osutils
@@ -91,6 +94,8 @@
 
   methods = router.ListMethods()
   if opts.service_method not in methods:
+    # Unknown method, try to match against known methods and make a suggestion.
+    # This is just for developer sanity, e.g. misspellings when testing.
     matched = matching.GetMostLikelyMatchedObject(methods, opts.service_method,
                                                   matched_score_threshold=0.6)
     error = 'Unrecognized service name.'
@@ -208,9 +213,12 @@
     if method_options.HasField('implementation_name'):
       method_name = method_options.implementation_name
 
-    # Check the chroot assertion settings before running.
+    # Check the chroot settings before running.
     service_options = svc.GetOptions().Extensions[self._service_options]
-    self._HandleChrootAssert(service_options, method_options)
+    if self._ChrootCheck(service_options, method_options):
+      # Run inside the chroot instead.
+      return self._ReexecuteInside(input_msg, output_path, service_name,
+                                   method_name)
 
     # Import the module and get the method.
     method_impl = self._GetMethod(module_name, method_name)
@@ -227,12 +235,18 @@
 
     return return_code
 
-  def _HandleChrootAssert(self, service_options, method_options):
-    """Check the chroot assert options and execute assertion as needed.
+  def _ChrootCheck(self, service_options, method_options):
+    """Check the chroot options, and execute assertion or note reexec as needed.
 
     Args:
       service_options (google.protobuf.Message): The service options.
       method_options (google.protobuf.Message): The method options.
+
+    Returns:
+      bool - True iff it needs to be reexeced inside the chroot.
+
+    Raises:
+      cros_build_lib.DieSystemExit when the chroot setting cannot be satisfied.
     """
     chroot_assert = build_api_pb2.NO_ASSERTION
     if method_options.HasField('method_chroot_assert'):
@@ -242,12 +256,76 @@
       # Fall back to the service option.
       chroot_assert = service_options.service_chroot_assert
 
-    # Execute appropriate assertion if set.
     if chroot_assert == build_api_pb2.INSIDE:
-      cros_build_lib.AssertInsideChroot()
+      return not cros_build_lib.IsInsideChroot()
     elif chroot_assert == build_api_pb2.OUTSIDE:
+      # If it must be run outside we have to already be outside.
       cros_build_lib.AssertOutsideChroot()
 
+    return False
+
+  def _ReexecuteInside(self, input_msg, output_path, service_name,
+                       method_name):
+    """Re-execute the service inside the chroot.
+
+    Args:
+      input_msg (Message): The parsed input message.
+      output_path (str): The path for the serialized output.
+      service_name (str): The name of the service to run.
+      method_name (str): The name of the method to run.
+    """
+    chroot_args = []
+    chroot_path = constants.DEFAULT_CHROOT_PATH
+    chroot_field_name = None
+    # Find the Chroot field. Search for the field by type to prevent it being
+    # tied to a naming convention.
+    for descriptor in input_msg.DESCRIPTOR.fields:
+      field = getattr(input_msg, descriptor.name)
+      if isinstance(field, common_pb2.Chroot):
+        chroot_field_name = descriptor.name
+        chroot = field
+        chroot_path = chroot.path
+        chroot_args.extend(self._GetChrootArgs(chroot))
+        break
+
+    base_dir = os.path.join(chroot_path, 'tmp')
+    with osutils.TempDir(base_dir=base_dir) as tempdir:
+      new_input = os.path.join(tempdir, 'input.json')
+      chroot_input = '/%s' % os.path.relpath(new_input, chroot_path)
+      new_output = os.path.join(tempdir, 'output.json')
+      chroot_output = '/%s' % os.path.relpath(new_output, chroot_path)
+
+      if chroot_field_name:
+        input_msg.ClearField(chroot_field_name)
+      osutils.WriteFile(new_input, json_format.MessageToJson(input_msg))
+      osutils.Touch(new_output)
+
+      cmd = ['build_api', '%s/%s' % (service_name, method_name),
+             '--input-json', chroot_input, '--output-json', chroot_output]
+      result = cros_build_lib.RunCommand(cmd, enter_chroot=True,
+                                         chroot_args=chroot_args,
+                                         error_code_ok=True)
+      shutil.move(new_output, output_path)
+
+      return result.returncode
+
+  def _GetChrootArgs(self, chroot):
+    """Translate a Chroot message to chroot enter args.
+
+    Args:
+      chroot (chromiumos.Chroot): A chroot message.
+
+    Returns:
+      list[str]: The cros_sdk args for the chroot.
+    """
+    args = []
+    if chroot.path:
+      args.extend(['--chroot', chroot.path])
+    if chroot.cache_dir:
+      args.extend(['--cache-dir', chroot.cache_dir])
+
+    return args
+
   def _GetMethod(self, module_name, method_name):
     """Get the implementation of the method for the service module.