factory_install: support installing partitions from installer resources

BUG=chrome-os-partner:36725
TEST=1. Build factory install shim for two boards
     2. Make RMA shim for two boards
     3. Merge two RMA shims: ./make_image_resource.sh --force usbimg1.bin usbimg2.bin
     4. Install the merged RMA shim on two boards, check it installs the right resources
CQ-DEPEND=CL:272418
CQ-DEPEND=CL:272568
CQ-DEPEND=CL:272881
CQ-DEPEND=CL:273100

Change-Id: Id65f0e6a2b24a1132808ad222ab7502bef8dd3b7
Reviewed-on: https://chromium-review.googlesource.com/272349
Reviewed-by: Mao Huang <littlecvr@chromium.org>
Tested-by: Bowgo Tsai <bowgotsai@chromium.org>
Commit-Queue: Bowgo Tsai <bowgotsai@chromium.org>
diff --git a/factory_install.sh b/factory_install.sh
index ffb9313..a55aeab 100644
--- a/factory_install.sh
+++ b/factory_install.sh
@@ -63,6 +63,10 @@
 # environment variable.
 ETH_INTERFACE=""
 
+# Mount point for installer resources.
+RESOURCES_MOUNT_POINT="$(mktemp -d)"
+USB_INSTALL_OFFSET="$(findLSBValue FACTORY_INSTALL_USB_OFFSET)"
+
 # Define our own logging function.  The one from memento_updater writes to
 # a file, which may cause a race condition if we tail it.
 log() {
@@ -623,62 +627,211 @@
   exit_success
 }
 
-factory_install_usb() {
-  local i=""
-  local src_offset="$(findLSBValue FACTORY_INSTALL_USB_OFFSET)"
-  local src_drive="$(findLSBValue REAL_USB_DEV)"
-  # REAL_USB_DEV is optional on systems without initramfs (ex, ARM).
-  [ -n "${src_drive}" ] || src_drive="$(rootdev -s 2>/dev/null)"
-  [ -n "${src_drive}" ] || die "Unknown media source. Please define REAL_USB_DEV."
+# When install from USB drive, the partition mapping from source USB drive
+# to the destination internal storage is as below.
+# src_partition: dst_partition
+# ----------------------------
+#  1:     1 [stateful]
+#  2:  None [install-kernel] for bootstrap, not installed to destination
+#  3:  None [install-rootfs] for bootstrap, not installed to destination
+#  4:     2 [factory-kernel] src_offset=2
+#  5:     3 [factory-rootfs] src_offset=2
+#  6:     4 [release-kernel] src_offset=2
+#  7:     5 [release-rootfs] src_offset=2
+#  8:     8 [oem]
+# 12:    12 [efi]
+# ----------------------------
+process_usb_install_mapping() {
+  local callback="$1"
+  shift
+  local param="$@" i
 
-  # Finds the real drive from sd[a-z][0-9]* or mmcblk[0-9]*p[0-9]*
-  src_drive="${src_drive%[0-9]*}"
-  src_drive="$(echo ${src_drive} | sed 's/\(mmcblk[0-9]*\)p/\1/')"
-
-  colorize yellow
   for i in EFI OEM STATE FACTORY FACTORY_KERNEL RELEASE RELEASE_KERNEL; do
-    # The source media must have exactly the same layout.
     local var="DST_${i}_PART"
     local part="${!var}"
-    local src="$(make_partition_dev ${src_drive} ${part})"
-    local dst="$(make_partition_dev ${DST_DRIVE} ${part})"
-
-    # Factory/Release may be shifted on source media.
-    if [ -n "${src_offset}" ]; then
-      case "${i}" in
-        FACTORY* | RELEASE* )
-          src="$(make_partition_dev ${src_drive} $((part + src_offset)) )"
-          true
-          ;;
-      esac
-    fi
-
-    # Detect file system size
-    local dd_param="bs=1M"
-    local count="$(dumpe2fs -h "${src}" 2>/dev/null |
-                   grep "^Block count" |
-                   sed 's/.*: *//')"
-
-    if [ -n "${count}" ]; then
-      local bs="$(dumpe2fs -h "${src}" 2>/dev/null |
-            grep "^Block size" |
-            sed 's/.*: *//')"
-
-      # Optimize copy speed, with restriction: bs<10M
-      while [ "$(( (count > 0) &&
-                   (count % 2 == 0) &&
-                   (bs / 1048576 < 10) ))" = "1" ]; do
-        count="$((count / 2))"
-        bs="$((bs * 2))"
-      done
-      dd_param="bs=${bs} count=${count}"
-    fi
-
-    log "Copying: [${i}] ${src} -> ${dst} (${dd_param})"
-    # TODO(hungte) Detect copy failure
-    pv -B 1M "${src}" |
-      dd ${dd_param} of="${dst}" iflag=fullblock oflag=dsync
+    local src_offset=""
+    case "${i}" in
+      FACTORY* | RELEASE* )
+        src_offset="${USB_INSTALL_OFFSET}"
+        ;;
+    esac
+    # Early return when callback function fails.
+    "${callback}" "${part}" "${src_offset}" ${param} || return 1
   done
+}
+
+# Callback of process_usb_install_mapping.
+# Copy a partition to DST_DRIVE by given partition number, src_offset
+# and src_drive.
+dst_drive_partition_copy() {
+  local part="$1"
+  local src_offset="$2"
+  local src_drive="$3"
+
+  local src="$(make_partition_dev "${src_drive}" "$((part + src_offset))")"
+  local dst="$(make_partition_dev "${DST_DRIVE}" "${part}")"
+
+  # Detect file system size
+  local dd_param="bs=1M"
+  local count="$(dumpe2fs -h "${src}" 2>/dev/null |
+                 grep "^Block count" |
+                 sed 's/.*: *//')"
+
+  if [ -n "${count}" ]; then
+    local bs="$(dumpe2fs -h "${src}" 2>/dev/null |
+                grep "^Block size" |
+                sed 's/.*: *//')"
+
+    # Optimize copy speed, with restriction: bs<10M
+    while [ "$(( (count > 0) &&
+                 (count % 2 == 0) &&
+                 (bs / 1048576 < 10) ))" = "1" ]; do
+      count="$((count / 2))"
+      bs="$((bs * 2))"
+    done
+    dd_param="bs=${bs} count=${count}"
+  fi
+
+  log "Copying: [${i}] ${src} -> ${dst} (${dd_param})"
+  # TODO(hungte) Detect copy failure
+  pv -B 1M "${src}" |
+    dd ${dd_param} of="${dst}" iflag=fullblock oflag=dsync
+}
+
+# Callback of process_usb_install_mapping.
+# Verify a image resource by given partition number, src_offset and
+# resources_dir.
+verify_resources_checksum() {
+  local part="$1"
+  local src_offset="$2"
+  local resources_dir="$3"
+  local resources_file=""
+
+  part=$(( part + src_offset ))
+  resources_file="${resources_dir}/${part}.gz"
+  [ -f "${resources_file}" ] || return 1
+
+  # The config file contains the checksum of each image file.
+  # For example:
+  #   1_checksum = HSQdstR/U3hoBpDtnHVEfdNgm/Y=
+  #   4_checksum = 3pt+EALoyH6hUmDbk735NY/MfAQ=
+  #   5_checksum = ZyDaPXjzJJK8i0hkIcsD4D+o2DE=
+  #   6_checksum = U/uLMDAkR3aP9oY/eD44K4ZReSg=
+  #   7_checksum = twHsO9+o8m0mn9KL3pKhzJATsHQ=
+  #   8_checksum = twHsO9+o8m0mn9KL3pKhzJATsHQ=
+  #  12_checksum = twHsO9+o8m0mn9KL3pKhzJATsHQ=
+  local config="${resources_dir}/config"
+  local expected_checksum="$(grep "${part}_checksum" "${config}" |
+                             cut -d" " -f3)"
+  local actual_checksum="$(openssl sha1 -binary "${resources_file}" |
+                           openssl base64)"
+  if [ "${actual_checksum}" = "${expected_checksum}" ]; then
+    return 0
+  else
+    return 1
+  fi
+}
+
+# Callback of process_usb_install_mapping.
+# Install a image resource to a partition on DST_DRIVE by given
+# partition number, src_offset and resources_dir.
+dst_drive_resources_install() {
+  local part="$1"
+  local src_offset="$2"
+  local resources_dir="$3"
+
+  local resource_file="${resources_dir}/$(( part + src_offset )).gz"
+  local dst="$(make_partition_dev "${DST_DRIVE}" "${part}")"
+
+  log "Installing: ${resource_file} -> ${dst}"
+  pv -B 1M "${resource_file}" | gunzip -c |
+    dd bs=1M of="${dst}" iflag=fullblock oflag=dsync
+}
+
+mount_installer_resources() {
+  local resources_drive="$1"
+  # Resources are stored in partition 1.
+  local resources_dev="$(make_partition_dev "${resources_drive}" 1)"
+  local resources_dir="${RESOURCES_MOUNT_POINT}/installer_resources"
+
+  mount -t ext4 -o ro "${resources_dev}" "${RESOURCES_MOUNT_POINT}"
+  if [ -d "${resources_dir}" ]; then
+    echo "${resources_dir}"
+  else
+    echo ""
+  fi
+}
+
+append_lsb_from_dst_drive() {
+  local lsb_file="dev_image/etc/lsb-factory"
+  local dst_path="/mnt/stateful_partition/${lsb_file}"
+  local state_dev="$(make_partition_dev "${DST_DRIVE}" 1)"
+  local state_mount="$(mktemp -d)"
+  local src_path="${state_mount}/${lsb_file}"
+
+  if ! mount "${state_dev}" "${state_mount}"; then
+    die "Failed to mount ${state_dev}!!"
+  fi
+
+  if [ -f "${src_path}" ]; then
+    cat "${src_path}" >> "${dst_path}"
+  else
+    die "Failed to find ${src_path}!!"
+  fi
+
+  umount "${state_mount}" || true
+  rmdir "${state_mount}" || true
+}
+
+factory_install_usb_resources() {
+  local installer_resources_dir="$1"
+  local kern_guid="$(findLSBValue KERN_ARG_KERN_GUID)"
+  local board_resources_dir="${installer_resources_dir}/${kern_guid}"
+
+  [ -n "${kern_guid}" ] || die "Kernel GUID not found in kernel cmdline."
+  [ -d "${board_resources_dir}" ] || die "Board resources not found."
+
+  # Verify resources checksum before installation.
+  log "Verifying resources checksum..."
+  process_usb_install_mapping \
+      verify_resources_checksum "${board_resources_dir}" ||
+    die "Resources checksum verification failed."
+  log "Done verifying checksum, start installation..."
+
+  # Install from board_resources_dir.
+  process_usb_install_mapping \
+      dst_drive_resources_install "${board_resources_dir}"
+
+  # Load board specific lsb-factory in DST_DRIVE for post installations.
+  # For example, FACTORY_INSTALL_FIRMWARE in lsb-factory.
+  append_lsb_from_dst_drive
+}
+
+factory_install_usb() {
+  local i="" installer_resources_dir=""
+  local src_drive="$(findLSBValue REAL_USB_DEV)"
+  # REAL_USB_DEV is optional on systems without initramfs (e.g., ARM).
+  [ -n "${src_drive}" ] || src_drive="$(rootdev -s 2>/dev/null)"
+  [ -n "${src_drive}" ] ||
+    die "Unknown media source. Please define REAL_USB_DEV."
+
+  # Finds the real drive by removing the partition numbers at the end.
+  # For example: sd[a-z][0-9]* -> sd[a-z]
+  #              mmcblk[0-9]*p[0-9]* -> mmcblk[0-9]*
+  src_drive="$(echo "${src_drive}" | sed 's/[0-9]\+$//')"
+  src_drive="$(echo "${src_drive}" | sed 's/\(mmcblk[0-9]*\)p/\1/')"
+
+  colorize yellow
+  installer_resources_dir="$(mount_installer_resources "${src_drive}")"
+  if [ -z "${installer_resources_dir}" ]; then
+    # Install from src_drive if installer resources are not found.
+    process_usb_install_mapping dst_drive_partition_copy "${src_drive}"
+  else
+    log "Installer resources found on USB stick."
+    # Install from installer resources.
+    factory_install_usb_resources "${installer_resources_dir}"
+  fi
+  umount "${RESOURCES_MOUNT_POINT}" || true
 
   # Run postinst on release partition to validate rootfs and create
   # verity table.