api: Add extra failure fields in Install* methods

Provide an expanded set of data points for each package which failed to
build in the InstallPackages and InstallToolchain endpoints.

BUG=b:204816060
TEST=unit

Change-Id: I2b7e6e2750b9d8dd6041acdc191198237f237335
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/3279481
Tested-by: Lizzy Presland <zland@google.com>
Auto-Submit: Lizzy Presland <zland@google.com>
Commit-Queue: Lizzy Presland <zland@google.com>
Reviewed-by: Alex Klein <saklein@chromium.org>
diff --git a/api/controller/sysroot_unittest.py b/api/controller/sysroot_unittest.py
index 9115eb1..cfae62d 100644
--- a/api/controller/sysroot_unittest.py
+++ b/api/controller/sysroot_unittest.py
@@ -247,6 +247,9 @@
     self.sysroot = os.path.join(self.tempdir, 'board')
     self.invalid_sysroot = os.path.join(self.tempdir, 'invalid', 'sysroot')
     osutils.SafeMakedirs(self.sysroot)
+    # Set up portage log directory.
+    self.portage_dir = os.path.join(self.sysroot, 'tmp', 'portage', 'logs')
+    osutils.SafeMakedirs(self.portage_dir)
 
   def _InputProto(self, build_target=None, sysroot_path=None,
                   compile_source=False):
@@ -265,6 +268,22 @@
     """Helper to build output proto instance."""
     return sysroot_pb2.InstallToolchainResponse()
 
+  def _CreatePortageLogFile(self, root, pkg_info, timestamp):
+    """Creates a log file for testing for individual packages built by Portage.
+
+    Args:
+      root (pathlike): the sysroot path
+      pkg_info (PackageInfo): name components for log file.
+      timestamp (datetime): timestamp used to name the file.
+    """
+    path = os.path.join(root, 'tmp', 'portage', 'logs',
+                        f'{pkg_info.category}:{pkg_info.package}:' \
+                        f'{timestamp.strftime("%Y%m%d-%H%M%S")}.log')
+    osutils.WriteFile(path,
+                      f'Test log file for package {pkg_info.category}/'
+                      f'{pkg_info.package} written to {path}')
+    return path
+
   def testValidateOnly(self):
     """Sanity check that a validate only call does not execute any logic."""
     patch = self.PatchObject(sysroot_service, 'InstallToolchain')
@@ -347,6 +366,16 @@
     err_pkgs = ['cat/pkg', 'cat2/pkg2']
     err_cpvs = [package_info.parse(pkg) for pkg in err_pkgs]
     expected = [('cat', 'pkg'), ('cat2', 'pkg2')]
+
+    new_logs = {}
+    for i, pkg in enumerate(err_pkgs):
+      self._CreatePortageLogFile(self.sysroot, err_cpvs[i],
+                                 datetime.datetime(2021, 6, 9, 13, 37, 0))
+      new_logs[pkg] = self._CreatePortageLogFile(self.sysroot, err_cpvs[i],
+                                                 datetime.datetime(2021, 6, 9,
+                                                                   16, 20, 0)
+                                                 )
+
     err = sysroot_lib.ToolchainInstallError('Error',
                                             cros_build_lib.CommandResult(),
                                             tc_info=err_cpvs)
@@ -356,6 +385,16 @@
                                              self.api_config)
     self.assertEqual(controller.RETURN_CODE_UNSUCCESSFUL_RESPONSE_AVAILABLE, rc)
     self.assertTrue(out_proto.failed_packages)
+    self.assertTrue(out_proto.failed_package_data)
+    # This needs to return 2 to indicate the available error response.
+    self.assertEqual(controller.RETURN_CODE_UNSUCCESSFUL_RESPONSE_AVAILABLE, rc)
+    for data in out_proto.failed_package_data:
+      package = controller_util.deserialize_package_info(data.name)
+      cat_pkg = (data.name.category, data.name.package_name)
+      self.assertIn(cat_pkg, expected)
+      self.assertEqual(data.log_path.path, new_logs[package.atom])
+
+    # TODO(b/206514844): remove when field is deleted
     for package in out_proto.failed_packages:
       cat_pkg = (package.category, package.package_name)
       self.assertIn(cat_pkg, expected)
@@ -372,6 +411,9 @@
     self.build_target = 'board'
     self.sysroot = os.path.join(self.tempdir, 'build', 'board')
     osutils.SafeMakedirs(self.sysroot)
+    # Set up portage log directory.
+    self.portage_dir = os.path.join(self.sysroot, 'tmp', 'portage', 'logs')
+    osutils.SafeMakedirs(self.portage_dir)
     # Set up goma directories.
     self.goma_dir = os.path.join(self.tempdir, 'goma_dir')
     osutils.SafeMakedirs(self.goma_dir)
@@ -428,6 +470,21 @@
         path,
         timestamp.strftime('Goma log file created at: %Y/%m/%d %H:%M:%S'))
 
+  def _CreatePortageLogFile(self, root, pkg_info, timestamp):
+    """Creates a log file for testing for individual packages built by Portage.
+
+    Args:
+      root (pathlike): the root path, taken from a BuildTarget object.
+      pkg_info (PackageInfo): name components for log file.
+      timestamp (datetime): timestamp used to name the file.
+    """
+    path = os.path.join(root, 'tmp', 'portage', 'logs',
+                        f'{pkg_info.category}:{pkg_info.package}:' \
+                        f'{timestamp.strftime("%Y%m%d-%H%M%S")}.log')
+    osutils.WriteFile(path, f'Test log file for package {pkg_info.category}/'
+                      f'{pkg_info.package} written to {path}')
+    return path
+
   def testValidateOnly(self):
     """Sanity check that a validate only call does not execute any logic."""
     patch = self.PatchObject(sysroot_service, 'BuildPackages')
@@ -685,9 +742,17 @@
 
     # Failed package info and expected list for verification.
     err_pkgs = ['cat/pkg', 'cat2/pkg2']
-    err_cpvs = [package_info.SplitCPV(cpv, strict=False) for cpv in err_pkgs]
+    err_cpvs = [package_info.parse(cpv) for cpv in err_pkgs]
     expected = [('cat', 'pkg'), ('cat2', 'pkg2')]
 
+    new_logs = {}
+    for i, pkg in enumerate(err_pkgs):
+      self._CreatePortageLogFile(self.sysroot, err_cpvs[i],
+                                 datetime.datetime(2021, 6, 9, 13, 37, 0))
+      new_logs[pkg] = self._CreatePortageLogFile(self.sysroot, err_cpvs[i],
+                                                 datetime.datetime(2021, 6, 9,
+                                                                   16, 20, 0)
+                                                 )
     # Force error to be raised with the packages.
     error = sysroot_lib.PackageInstallError('Error',
                                             cros_build_lib.CommandResult(),
@@ -698,6 +763,13 @@
                                             self.api_config)
     # This needs to return 2 to indicate the available error response.
     self.assertEqual(controller.RETURN_CODE_UNSUCCESSFUL_RESPONSE_AVAILABLE, rc)
+    for data in out_proto.failed_package_data:
+      package = controller_util.deserialize_package_info(data.name)
+      cat_pkg = (data.name.category, data.name.package_name)
+      self.assertIn(cat_pkg, expected)
+      self.assertEqual(data.log_path.path, new_logs[package.atom])
+
+    # TODO(b/206514844): remove when field is deleted
     for package in out_proto.failed_packages:
       cat_pkg = (package.category, package.package_name)
       self.assertIn(cat_pkg, expected)