cli: DLC LoadPin deploy support

This allows developers to deploy their DLC packages which are LoadPin
dependent for development without having to rebuild an entire image
repeatedly.

BUG=b:241961281
TEST=./run_tests -- -x cli/deploy_unittest.py

Change-Id: Ie96ed7320e91c001d9c259033aae8eca711c3894
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/3872386
Commit-Queue: Jae Hoon Kim <kimjae@chromium.org>
Tested-by: Jae Hoon Kim <kimjae@chromium.org>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
diff --git a/cli/deploy.py b/cli/deploy.py
index 3ae3525..81a20e8 100644
--- a/cli/deploy.py
+++ b/cli/deploy.py
@@ -16,6 +16,7 @@
 import json
 import logging
 import os
+from pathlib import Path
 import tempfile
 
 from chromite.cli import command
@@ -1043,52 +1044,82 @@
 
 
 def _DeployDLCImage(device, sysroot, board, dlc_id, dlc_package):
-  """Deploy (install and mount) a DLC image."""
-  # Build the DLC image if the image is outdated or doesn't exist.
-  dlc_lib.InstallDlcImages(sysroot=sysroot, dlc_id=dlc_id, board=board)
+  """Deploy (install and mount) a DLC image.
 
-  logging.debug('Uninstall DLC %s if it is installed.', dlc_id)
-  try:
-    device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
-  except cros_build_lib.RunCommandError as e:
-    logging.info('Failed to uninstall DLC:%s. Continue anyway.', e.stderr)
-  except Exception:
-    logging.error('Failed to uninstall DLC.')
-    raise
+  Args:
+    device: A device object.
+    sysroot: The sysroot path.
+    board: Board to use.
+    dlc_id: The DLC ID.
+    dlc_package: The DLC package name.
+  """
+  # Requires `sudo_rm` because installations of files are running with sudo.
+  with osutils.TempDir(sudo_rm=True) as tempdir:
+    temp_rootfs = Path(tempdir)
+    # Build the DLC image if the image is outdated or doesn't exist.
+    dlc_lib.InstallDlcImages(
+        sysroot=sysroot, rootfs=temp_rootfs, dlc_id=dlc_id, board=board)
 
-  # TODO(andrewlassalle): Copy the DLC image to the preload location instead
-  # of to dlc_a and dlc_b, and let dlcserive install the images to their final
-  # location.
-  logging.notice('Deploy the DLC image for %s', dlc_id)
-  dlc_img_path_src = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
-                                  dlc_package, dlc_lib.DLC_IMAGE)
-  dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
-  dlc_img_path_a = os.path.join(dlc_img_path, 'dlc_a')
-  dlc_img_path_b = os.path.join(dlc_img_path, 'dlc_b')
-  # Create directories for DLC images.
-  device.run(['mkdir', '-p', dlc_img_path_a, dlc_img_path_b])
-  # Copy images to the destination directories.
-  device.CopyToDevice(dlc_img_path_src, os.path.join(dlc_img_path_a,
-                                                     dlc_lib.DLC_IMAGE),
-                      mode='rsync')
-  device.run(['cp', os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
-              os.path.join(dlc_img_path_b, dlc_lib.DLC_IMAGE)])
+    logging.debug('Uninstall DLC %s if it is installed.', dlc_id)
+    try:
+      device.run(['dlcservice_util', '--uninstall', '--id=%s' % dlc_id])
+    except cros_build_lib.RunCommandError as e:
+      logging.info('Failed to uninstall DLC:%s. Continue anyway.', e.stderr)
+    except Exception:
+      logging.error('Failed to uninstall DLC.')
+      raise
 
-  # Set the proper perms and ownership so dlcservice can access the image.
-  device.run(['chmod', '-R', 'u+rwX,go+rX,go-w', _DLC_INSTALL_ROOT])
-  device.run(['chown', '-R', 'dlcservice:dlcservice', _DLC_INSTALL_ROOT])
+    # TODO(andrewlassalle): Copy the DLC image to the preload location instead
+    # of to dlc_a and dlc_b, and let dlcserive install the images to their final
+    # location.
+    logging.notice('Deploy the DLC image for %s', dlc_id)
+    dlc_img_path_src = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
+                                    dlc_package, dlc_lib.DLC_IMAGE)
+    dlc_img_path = os.path.join(_DLC_INSTALL_ROOT, dlc_id, dlc_package)
+    dlc_img_path_a = os.path.join(dlc_img_path, 'dlc_a')
+    dlc_img_path_b = os.path.join(dlc_img_path, 'dlc_b')
+    # Create directories for DLC images.
+    device.run(['mkdir', '-p', dlc_img_path_a, dlc_img_path_b])
+    # Copy images to the destination directories.
+    device.CopyToDevice(dlc_img_path_src, os.path.join(dlc_img_path_a,
+                                                       dlc_lib.DLC_IMAGE),
+                        mode='rsync')
+    device.run(['cp', os.path.join(dlc_img_path_a, dlc_lib.DLC_IMAGE),
+                os.path.join(dlc_img_path_b, dlc_lib.DLC_IMAGE)])
 
-  # Copy metadata to device.
-  dest_mata_dir = os.path.join('/', dlc_lib.DLC_META_DIR, dlc_id,
-                               dlc_package)
-  device.run(['mkdir', '-p', dest_mata_dir])
-  src_meta_dir = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
-                              dlc_package, dlc_lib.DLC_TMP_META_DIR)
-  device.CopyToDevice(src_meta_dir + '/',
-                      dest_mata_dir,
-                      mode='rsync',
-                      recursive=True,
-                      remote_sudo=True)
+    # Set the proper perms and ownership so dlcservice can access the image.
+    device.run(['chmod', '-R', 'u+rwX,go+rX,go-w', _DLC_INSTALL_ROOT])
+    device.run(['chown', '-R', 'dlcservice:dlcservice', _DLC_INSTALL_ROOT])
+
+    # Copy metadata to device.
+    dest_meta_dir = Path('/') / dlc_lib.DLC_META_DIR / dlc_id / dlc_package
+    device.run(['mkdir', '-p', str(dest_meta_dir)])
+    src_meta_dir = os.path.join(sysroot, dlc_lib.DLC_BUILD_DIR, dlc_id,
+                                dlc_package, dlc_lib.DLC_TMP_META_DIR)
+    device.CopyToDevice(src_meta_dir + '/',
+                        dest_meta_dir,
+                        mode='rsync',
+                        recursive=True,
+                        remote_sudo=True)
+
+    # TODO(kimjae): Make this generic so it recomputes all the DLCs + copies
+    # over a fresh list of dm-verity digests instead of appending and keeping
+    # the stale digests when developers are testing.
+
+    # Copy the LoadPin dm-verity digests to device.
+    loadpin = dlc_lib.DLC_LOADPIN_TRUSTED_VERITY_DIGESTS
+    dst_loadpin = Path('/') / dlc_lib.DLC_META_DIR / loadpin
+    src_loadpin = temp_rootfs / dlc_lib.DLC_META_DIR / loadpin
+    if src_loadpin.exists():
+      digests = set(osutils.ReadFile(src_loadpin).split())
+      try:
+        digests.update(device.CatFile(dst_loadpin).split())
+      except remote_access.CatFileError:
+        pass
+
+      with tempfile.NamedTemporaryFile(dir=temp_rootfs) as f:
+        osutils.WriteFile(f.name, '\n'.join(digests))
+        device.CopyToDevice(f.name, dst_loadpin, mode='rsync', remote_sudo=True)
 
 
 def _GetDLCInfo(device, pkg_path, from_dut):