update_engine: Scaled DLC installation

Expose scaled DLC installation, which will fetch DLC images/payloads
from Bandaid(+Lorry) URL paths with DLC slotting.

Lorry is used as a fallback/backup of Bandaid URIs.

When DLCs are installed, the specific offset and SHA256 hash are hard
requirement to match that of rootfs before further downstreams use with
device mapper features. (This step should save downstream folks from
requiring secondary verification and reduce repeated hashings)

BUG=b:236008158
TEST=FEATURES=test emerge-$B update_engine

Cq-Depend: chromium:3972005, chromium:3979946
Change-Id: Ifaab202a0185471106285e5c90c3aa31d977fe34
Reviewed-on: https://chromium-review.googlesource.com/c/aosp/platform/system/update_engine/+/3977212
Reviewed-by: Henry Barnor <[email protected]>
Reviewed-by: Yuanpeng Ni‎ <[email protected]>
Tested-by: Jae Hoon Kim <[email protected]>
diff --git a/BUILD.gn b/BUILD.gn
index 47f8f70..63ecda7 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -194,6 +194,7 @@
     "cros/download_action_chromeos.cc",
     "cros/hardware_chromeos.cc",
     "cros/image_properties_chromeos.cc",
+    "cros/install_action.cc",
     "cros/logging.cc",
     "cros/metrics_reporter_omaha.cc",
     "cros/omaha_parser_data.cc",
@@ -260,6 +261,7 @@
     "expat",
     "libcurl",
     "libdebugd-client",
+    "libimageloader-manifest",
     "libmetrics",
     "libpower_manager-client",
     "libsession_manager-client",
@@ -499,6 +501,7 @@
       "cros/download_action_chromeos_unittest.cc",
       "cros/hardware_chromeos_unittest.cc",
       "cros/image_properties_chromeos_unittest.cc",
+      "cros/install_action_test.cc",
       "cros/metrics_reporter_omaha_unittest.cc",
       "cros/omaha_request_action_unittest.cc",
       "cros/omaha_request_builder_xml_unittest.cc",
diff --git a/client_library/client_dbus.cc b/client_library/client_dbus.cc
index d5a371d..b3b4287 100644
--- a/client_library/client_dbus.cc
+++ b/client_library/client_dbus.cc
@@ -93,6 +93,11 @@
   return proxy_->AttemptInstall(omaha_url, dlc_ids, nullptr);
 }
 
+bool DBusUpdateEngineClient::Install(
+    const update_engine::InstallParams& install_params) {
+  return proxy_->Install(install_params, nullptr);
+}
+
 bool DBusUpdateEngineClient::SetDlcActiveValue(bool is_active,
                                                const std::string& dlc_id) {
   return proxy_->SetDlcActiveValue(is_active, dlc_id, /*error=*/nullptr);
diff --git a/client_library/client_dbus.h b/client_library/client_dbus.h
index 48900e8..4bb58d0 100644
--- a/client_library/client_dbus.h
+++ b/client_library/client_dbus.h
@@ -47,6 +47,8 @@
   bool AttemptInstall(const std::string& omaha_url,
                       const std::vector<std::string>& dlc_ids) override;
 
+  bool Install(const update_engine::InstallParams& install_params) override;
+
   bool SetDlcActiveValue(bool is_active, const std::string& dlc_id) override;
 
   bool GetStatus(UpdateEngineStatus* out_status) const override;
diff --git a/client_library/include/update_engine/client.h b/client_library/include/update_engine/client.h
index 2ec2113..c9dbec5 100644
--- a/client_library/include/update_engine/client.h
+++ b/client_library/include/update_engine/client.h
@@ -53,6 +53,7 @@
   //     A list of DLC module IDs.
   virtual bool AttemptInstall(const std::string& omaha_url,
                               const std::vector<std::string>& dlc_ids) = 0;
+  virtual bool Install(const update_engine::InstallParams& install_params) = 0;
 
   // Returns the entire update engine status struct.
   virtual bool GetStatus(UpdateEngineStatus* out_status) const = 0;
diff --git a/common/error_code.h b/common/error_code.h
index b8a1f84..26bd1a9 100644
--- a/common/error_code.h
+++ b/common/error_code.h
@@ -90,6 +90,7 @@
   kRepeatedFpFromOmahaError = 64,
   kInvalidateLastUpdate = 65,
   kOmahaUpdateIgnoredOverMetered = 66,
+  kScaledInstallationError = 67,
 
   // VERY IMPORTANT! When adding new error codes:
   //
diff --git a/common/error_code_utils.cc b/common/error_code_utils.cc
index 258d136..8af51c2 100644
--- a/common/error_code_utils.cc
+++ b/common/error_code_utils.cc
@@ -181,6 +181,8 @@
       return "ErrorCode::kInvalidateLastUpdate";
     case ErrorCode::kOmahaUpdateIgnoredOverMetered:
       return "ErrorCode::kOmahaUpdateIgnoredOverMetered";
+    case ErrorCode::kScaledInstallationError:
+      return "ErrorCode::kScaledInstallationError";
       // Don't add a default case to let the compiler warn about newly added
       // error codes which should be added here.
   }
diff --git a/cros/common_service.cc b/cros/common_service.cc
index 8cb040b..9db6793 100644
--- a/cros/common_service.cc
+++ b/cros/common_service.cc
@@ -95,10 +95,25 @@
 bool UpdateEngineService::AttemptInstall(brillo::ErrorPtr* error,
                                          const string& omaha_url,
                                          const vector<string>& dlc_ids) {
-  if (!SystemState::Get()->update_attempter()->CheckForInstall(dlc_ids,
-                                                               omaha_url)) {
+  if (!SystemState::Get()->update_attempter()->CheckForInstall(
+          dlc_ids,
+          omaha_url,
+          /*scaled=*/false)) {
     // TODO(xiaochu): support more detailed error messages.
-    LogAndSetError(error, FROM_HERE, "Could not schedule install operation.");
+    LogAndSetError(error, FROM_HERE, "Could not schedule install.");
+    return false;
+  }
+  return true;
+}
+
+bool UpdateEngineService::Install(
+    brillo::ErrorPtr* error,
+    const update_engine::InstallParams& install_params) {
+  if (!SystemState::Get()->update_attempter()->CheckForInstall(
+          {install_params.id()},
+          install_params.omaha_url(),
+          install_params.scaled())) {
+    LogAndSetError(error, FROM_HERE, "Could not schedule scaled install.");
     return false;
   }
   return true;
diff --git a/cros/common_service.h b/cros/common_service.h
index d932d08..07d30fe 100644
--- a/cros/common_service.h
+++ b/cros/common_service.h
@@ -54,6 +54,9 @@
                       const std::string& omaha_url,
                       const std::vector<std::string>& dlc_ids);
 
+  bool Install(brillo::ErrorPtr* error,
+               const update_engine::InstallParams& install_params);
+
   bool AttemptRollback(brillo::ErrorPtr* error, bool in_powerwash);
 
   // Checks if the system rollback is available by verifying if the secondary
diff --git a/cros/common_service_unittest.cc b/cros/common_service_unittest.cc
index f195120..9c02ec3 100644
--- a/cros/common_service_unittest.cc
+++ b/cros/common_service_unittest.cc
@@ -95,7 +95,7 @@
 }
 
 TEST_F(UpdateEngineServiceTest, AttemptInstall) {
-  EXPECT_CALL(*mock_update_attempter_, CheckForInstall(_, _))
+  EXPECT_CALL(*mock_update_attempter_, CheckForInstall(_, _, _))
       .WillOnce(Return(true));
 
   EXPECT_TRUE(common_service_.AttemptInstall(&error_, "", {}));
@@ -103,7 +103,7 @@
 }
 
 TEST_F(UpdateEngineServiceTest, AttemptInstallReturnsFalse) {
-  EXPECT_CALL(*mock_update_attempter_, CheckForInstall(_, _))
+  EXPECT_CALL(*mock_update_attempter_, CheckForInstall(_, _, _))
       .WillOnce(Return(false));
 
   EXPECT_FALSE(common_service_.AttemptInstall(&error_, "", {}));
diff --git a/cros/dbus_service.cc b/cros/dbus_service.cc
index 924498c..668ba3a 100644
--- a/cros/dbus_service.cc
+++ b/cros/dbus_service.cc
@@ -89,6 +89,11 @@
   return common_->AttemptInstall(error, in_omaha_url, dlc_ids);
 }
 
+bool DBusUpdateEngineService::Install(
+    ErrorPtr* error, const update_engine::InstallParams& install_params) {
+  return common_->Install(error, install_params);
+}
+
 bool DBusUpdateEngineService::AttemptRollback(ErrorPtr* error,
                                               bool in_powerwash) {
   return common_->AttemptRollback(error, in_powerwash);
diff --git a/cros/dbus_service.h b/cros/dbus_service.h
index f583671..73b6a88 100644
--- a/cros/dbus_service.h
+++ b/cros/dbus_service.h
@@ -55,6 +55,9 @@
                       const std::string& in_omaha_url,
                       const std::vector<std::string>& dlc_ids) override;
 
+  bool Install(brillo::ErrorPtr* err,
+               const update_engine::InstallParams& install_params) override;
+
   bool AttemptRollback(brillo::ErrorPtr* error, bool in_powerwash) override;
 
   // Checks if the system rollback is available by verifying if the secondary
diff --git a/cros/image_properties.h b/cros/image_properties.h
index 1297547..c2ee0df 100644
--- a/cros/image_properties.h
+++ b/cros/image_properties.h
@@ -58,6 +58,9 @@
 
   // The Omaha URL this image should get updates from.
   std::string omaha_url;
+
+  // The release builder path.
+  std::string builder_path;
 };
 
 // The mutable image properties are read-write image properties, initialized
diff --git a/cros/image_properties_chromeos.cc b/cros/image_properties_chromeos.cc
index 79155b5..2e159f0 100644
--- a/cros/image_properties_chromeos.cc
+++ b/cros/image_properties_chromeos.cc
@@ -31,16 +31,18 @@
 
 namespace {
 
-const char kLsbRelease[] = "/etc/lsb-release";
+constexpr char kLsbRelease[] = "/etc/lsb-release";
 
-const char kLsbReleaseAppIdKey[] = "CHROMEOS_RELEASE_APPID";
-const char kLsbReleaseAutoUpdateServerKey[] = "CHROMEOS_AUSERVER";
-const char kLsbReleaseBoardAppIdKey[] = "CHROMEOS_BOARD_APPID";
-const char kLsbReleaseBoardKey[] = "CHROMEOS_RELEASE_BOARD";
-const char kLsbReleaseCanaryAppIdKey[] = "CHROMEOS_CANARY_APPID";
-const char kLsbReleaseIsPowerwashAllowedKey[] = "CHROMEOS_IS_POWERWASH_ALLOWED";
-const char kLsbReleaseUpdateChannelKey[] = "CHROMEOS_RELEASE_TRACK";
-const char kLsbReleaseVersionKey[] = "CHROMEOS_RELEASE_VERSION";
+constexpr char kLsbReleaseAppIdKey[] = "CHROMEOS_RELEASE_APPID";
+constexpr char kLsbReleaseAutoUpdateServerKey[] = "CHROMEOS_AUSERVER";
+constexpr char kLsbReleaseBoardAppIdKey[] = "CHROMEOS_BOARD_APPID";
+constexpr char kLsbReleaseBoardKey[] = "CHROMEOS_RELEASE_BOARD";
+constexpr char kLsbReleaseBuilderPath[] = "CHROMEOS_RELEASE_BUILDER_PATH";
+constexpr char kLsbReleaseCanaryAppIdKey[] = "CHROMEOS_CANARY_APPID";
+constexpr char kLsbReleaseIsPowerwashAllowedKey[] =
+    "CHROMEOS_IS_POWERWASH_ALLOWED";
+constexpr char kLsbReleaseUpdateChannelKey[] = "CHROMEOS_RELEASE_TRACK";
+constexpr char kLsbReleaseVersionKey[] = "CHROMEOS_RELEASE_VERSION";
 
 const char kDefaultAppId[] = "{87efface-864d-49a5-9bb3-4b050a7c227a}";
 
@@ -117,6 +119,8 @@
       GetStringWithDefault(lsb_release,
                            kLsbReleaseAutoUpdateServerKey,
                            constants::kOmahaDefaultProductionURL);
+  result.builder_path =
+      GetStringWithDefault(lsb_release, kLsbReleaseBuilderPath, "");
   // Build fingerprint not used in Chrome OS.
   result.build_fingerprint = "";
   result.allow_arbitrary_channels = false;
diff --git a/cros/install_action.cc b/cros/install_action.cc
new file mode 100644
index 0000000..74e6c92
--- /dev/null
+++ b/cros/install_action.cc
@@ -0,0 +1,228 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include "update_engine/cros/install_action.h"
+
+#include <inttypes.h>
+
+#include <string>
+#include <utility>
+
+#include <base/files/file_path.h>
+#include <base/files/file_util.h>
+#include <base/logging.h>
+#include <base/strings/string_number_conversions.h>
+#include <chromeos/constants/imageloader.h>
+#include <crypto/secure_hash.h>
+#include <crypto/sha2.h>
+#include <libimageloader/manifest.h>
+
+#include "update_engine/common/boot_control.h"
+#include "update_engine/common/system_state.h"
+#include "update_engine/cros/image_properties.h"
+
+namespace chromeos_update_engine {
+
+namespace {
+constexpr char kBandaidUrl[] = "https://redirector.gvt1.com/edgedl/dlc";
+constexpr char kLorryUrl[] = "https://dl.google.com/dlc";
+
+constexpr char kDefaultArtifact[] = "dlc.img";
+constexpr char kDefaultPackage[] = "package";
+constexpr char kDefaultSlotting[] = "dlc";
+
+constexpr char kManifestFile[] = "imageloader.json";
+
+std::shared_ptr<imageloader::Manifest> LoadManifest(
+    const std::string& manifest_dir,
+    const std::string& id,
+    const std::string& package) {
+  std::string json_str;
+  auto manifest_path = base::FilePath(manifest_dir)
+                           .Append(id)
+                           .Append(package)
+                           .Append(kManifestFile);
+
+  if (!base::ReadFileToString(manifest_path, &json_str)) {
+    LOG(ERROR) << "Failed to read manifest at " << manifest_path.value();
+    return nullptr;
+  }
+
+  auto manifest = std::make_shared<imageloader::Manifest>();
+  if (!manifest->ParseManifest(json_str)) {
+    LOG(ERROR) << "Failed to parse manifest for DLC=" << id;
+    return nullptr;
+  }
+
+  return manifest;
+}
+}  // namespace
+
+InstallAction::InstallAction(std::unique_ptr<HttpFetcher> http_fetcher,
+                             const std::string& id,
+                             const std::string& slotting,
+                             const std::string& manifest_dir)
+    : http_fetcher_(std::move(http_fetcher)),
+      id_(id),
+      backup_urls_({kLorryUrl}) {
+  slotting_ = slotting.empty() ? kDefaultSlotting : slotting;
+  manifest_dir_ =
+      manifest_dir.empty() ? imageloader::kDlcManifestRootpath : manifest_dir;
+}
+
+InstallAction::~InstallAction() {}
+
+void InstallAction::PerformAction() {
+  LOG(INFO) << "InstallAction performing action.";
+
+  manifest_ = LoadManifest(manifest_dir_, id_, kDefaultPackage);
+  if (!manifest_) {
+    LOG(ERROR) << "Could not retrieve manifest for " << id_;
+    processor_->ActionComplete(this, ErrorCode::kScaledInstallationError);
+    return;
+  }
+  image_props_ = LoadImageProperties();
+  http_fetcher_->set_delegate(this);
+
+  // Get the DLC device partition.
+  auto partition_name =
+      base::FilePath("dlc").Append(id_).Append(kDefaultPackage).value();
+  auto* boot_control = SystemState::Get()->boot_control();
+
+  std::string partition;
+  if (!boot_control->GetPartitionDevice(
+          partition_name, boot_control->GetCurrentSlot(), &partition)) {
+    LOG(ERROR) << "Could not retrieve device partition for " << id_;
+    processor_->ActionComplete(this, ErrorCode::kScaledInstallationError);
+    return;
+  }
+
+  f_.Initialize(base::FilePath(partition),
+                base::File::Flags::FLAG_OPEN | base::File::Flags::FLAG_READ |
+                    base::File::Flags::FLAG_WRITE);
+  if (!f_.IsValid()) {
+    LOG(ERROR) << "Could not open device partition for " << id_ << " at "
+               << partition;
+    processor_->ActionComplete(this, ErrorCode::kScaledInstallationError);
+    return;
+  }
+  LOG(INFO) << "Installing to " << partition;
+  StartInstallation(kBandaidUrl);
+}
+
+void InstallAction::TerminateProcessing() {
+  http_fetcher_->TerminateTransfer();
+}
+
+bool InstallAction::ReceivedBytes(HttpFetcher* fetcher,
+                                  const void* bytes,
+                                  size_t length) {
+  uint64_t new_offset = offset_ + length;
+  // Overflow upper bound check against manifest.
+  if (new_offset > manifest_->size()) {
+    LOG(ERROR) << "Overflow of bytes, terminating.";
+    http_fetcher_->TerminateTransfer();
+    return false;
+  }
+
+  if (delegate()) {
+    delegate_->BytesReceived(new_offset, manifest_->size());
+  }
+
+  hash_->Update(bytes, length);
+  int64_t total_written_bytes = 0;
+  do {
+    int written_bytes =
+        f_.Write(offset_ + total_written_bytes,
+                 static_cast<const char*>(bytes) + total_written_bytes,
+                 length - total_written_bytes);
+    if (written_bytes == -1) {
+      PLOG(ERROR) << "Failed to write bytes.";
+      http_fetcher_->TerminateTransfer();
+      return false;
+    }
+
+    total_written_bytes += written_bytes;
+  } while (total_written_bytes != length);
+
+  offset_ = new_offset;
+  return true;
+}
+
+void InstallAction::TransferComplete(HttpFetcher* fetcher, bool successful) {
+  if (!successful) {
+    LOG(ERROR) << "Transfer failed.";
+    http_fetcher_->TerminateTransfer();
+    return;
+  }
+
+  auto expected_offset = manifest_->size();
+  if (offset_ != expected_offset) {
+    LOG(ERROR) << "Transferred bytes offset (" << offset_
+               << ") don't match the expected offset (" << expected_offset
+               << ").";
+    http_fetcher_->TerminateTransfer();
+    return;
+  }
+  LOG(INFO) << "Transferred bytes offset (" << expected_offset << ") is valid.";
+
+  std::vector<uint8_t> sha256(crypto::kSHA256Length);
+  hash_->Finish(sha256.data(), sha256.size());
+  auto expected_sha256 = manifest_->image_sha256();
+  auto expected_sha256_str =
+      base::HexEncode(expected_sha256.data(), expected_sha256.size());
+  if (sha256 != expected_sha256) {
+    LOG(ERROR) << "Transferred bytes hash ("
+               << base::HexEncode(sha256.data(), sha256.size())
+               << ") don't match the expected hash (" << expected_sha256_str
+               << ").";
+    http_fetcher_->TerminateTransfer();
+    return;
+  }
+  LOG(INFO) << "Transferred bytes hash (" << expected_sha256_str
+            << ") is valid.";
+
+  processor_->ActionComplete(this, ErrorCode::kSuccess);
+}
+
+void InstallAction::TransferTerminated(HttpFetcher* fetcher) {
+  // Continue to use backup URLs.
+  if (backup_url_index_ < backup_urls_.size()) {
+    LOG(INFO) << "Using backup url at index=" << backup_url_index_;
+    StartInstallation(backup_urls_[backup_url_index_++]);
+    return;
+  }
+  LOG(ERROR) << "Failed to complete transfer.";
+  processor_->ActionComplete(this, ErrorCode::kScaledInstallationError);
+}
+
+void InstallAction::StartInstallation(const std::string& url) {
+  offset_ = 0;
+  hash_.reset(crypto::SecureHash::Create(crypto::SecureHash::SHA256));
+  auto url_to_fetch = base::FilePath(url)
+                          .Append(image_props_.builder_path)
+                          .Append(slotting_)
+                          .Append(id_)
+                          .Append(kDefaultPackage)
+                          .Append(kDefaultArtifact)
+                          .value();
+  LOG(INFO) << "Starting installation using URL=" << url_to_fetch;
+  http_fetcher_->SetOffset(0);
+  http_fetcher_->UnsetLength();
+  http_fetcher_->BeginTransfer(url_to_fetch);
+}
+
+}  // namespace chromeos_update_engine
diff --git a/cros/install_action.h b/cros/install_action.h
new file mode 100644
index 0000000..67c181f
--- /dev/null
+++ b/cros/install_action.h
@@ -0,0 +1,133 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#ifndef UPDATE_ENGINE_CROS_INSTALL_ACTION_H_
+#define UPDATE_ENGINE_CROS_INSTALL_ACTION_H_
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <base/files/file.h>
+#include <crypto/secure_hash.h>
+#include <gtest/gtest_prod.h>  // for FRIEND_TEST
+#include <libimageloader/manifest.h>
+
+#include "update_engine/common/action.h"
+#include "update_engine/common/http_fetcher.h"
+#include "update_engine/cros/image_properties.h"
+
+// The Installation action flow for scaled DLC(s).
+
+namespace chromeos_update_engine {
+
+class NoneType;
+class InstallAction;
+class OmahaRequestParams;
+
+template <>
+class ActionTraits<InstallAction> {
+ public:
+  // No input/output objects.
+  typedef NoneType InputObjectType;
+  typedef NoneType OutputObjectType;
+};
+
+class InstallActionDelegate {
+ public:
+  virtual ~InstallActionDelegate() = default;
+
+  // Called periodically after bytes are received.
+  // `bytes_received` is the total number of bytes installed.
+  // `total` is the target bytes to install.
+  virtual void BytesReceived(uint64_t bytes_received, uint64_t total) = 0;
+};
+
+class InstallAction : public Action<InstallAction>, public HttpFetcherDelegate {
+ public:
+  // Args:
+  //  http_fetcher: An HttpFetcher to take ownership of. Injected for testing.
+  //  id: The DLC ID to install.
+  //  slotting: Override of scaled DLC slotting to use, empty to use default.
+  InstallAction(std::unique_ptr<HttpFetcher> http_fetcher,
+                const std::string& id,
+                const std::string& slotting = "",
+                const std::string& manifest_dir = "");
+  InstallAction(const InstallAction&) = delete;
+  InstallAction& operator=(const InstallAction&) = delete;
+
+  ~InstallAction() override;
+  typedef ActionTraits<InstallAction>::InputObjectType InputObjectType;
+  typedef ActionTraits<InstallAction>::OutputObjectType OutputObjectType;
+  void PerformAction() override;
+  void TerminateProcessing() override;
+
+  int GetHTTPResponseCode() { return http_fetcher_->http_response_code(); }
+
+  // Debugging/logging
+  static std::string StaticType() { return "InstallAction"; }
+  std::string Type() const override { return StaticType(); }
+
+  // Delegate methods (see http_fetcher.h)
+  bool ReceivedBytes(HttpFetcher* fetcher,
+                     const void* bytes,
+                     size_t length) override;
+  void TransferComplete(HttpFetcher* fetcher, bool successful) override;
+  void TransferTerminated(HttpFetcher* fetcher) override;
+
+  InstallActionDelegate* delegate() const { return delegate_; }
+  void set_delegate(InstallActionDelegate* delegate) { delegate_ = delegate; }
+
+ private:
+  void StartInstallation(const std::string& url);
+
+  InstallActionDelegate* delegate_{nullptr};
+
+  // Hasher to hash as artifacts get fetched.
+  std::unique_ptr<crypto::SecureHash> hash_;
+
+  ImageProperties image_props_;
+
+  // The HTTP fetcher given ownership to.
+  std::unique_ptr<HttpFetcher> http_fetcher_;
+
+  // The DLC ID.
+  std::string id_;
+
+  // The Lorry slotting to use for fetches.
+  std::string slotting_;
+
+  // Offset into `f_` that are being written to, it's faster to cache instead of
+  // lseek'ing on the offset.
+  int64_t offset_{0};
+  base::File f_;
+
+  // The list of backup URLs.
+  std::vector<std::string> backup_urls_;
+  int backup_url_index_{0};
+
+  // The DLC manifest accessor.
+  std::shared_ptr<imageloader::Manifest> manifest_;
+  std::string manifest_dir_;
+};
+
+}  // namespace chromeos_update_engine
+
+#endif  // UPDATE_ENGINE_CROS_INSTALL_ACTION_H_
diff --git a/cros/install_action_test.cc b/cros/install_action_test.cc
new file mode 100644
index 0000000..58d3985
--- /dev/null
+++ b/cros/install_action_test.cc
@@ -0,0 +1,261 @@
+//
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include "update_engine/cros/install_action.h"
+
+#include <utility>
+#include <vector>
+
+#include <base/files/file_util.h>
+#include <base/strings/stringprintf.h>
+#include <brillo/message_loops/fake_message_loop.h>
+#include <gtest/gtest.h>
+
+#include "update_engine/common/action_processor.h"
+#include "update_engine/common/mock_http_fetcher.h"
+#include "update_engine/common/test_utils.h"
+#include "update_engine/cros/fake_system_state.h"
+
+namespace chromeos_update_engine {
+
+namespace {
+constexpr char kDefaultOffset[] = "1024";
+constexpr char kDefaultSha[] =
+    "5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef";
+
+constexpr char kManifestTemplate[] =
+    R"({
+  "critical-update": false,
+  "days-to-purge": 5,
+  "description": "A FOOBAR DLC",
+  "factory-install": false,
+  "fs-type": "squashfs",
+  "id": "sample-dlc",
+  "image-sha256-hash": "%s",
+  "image-type": "dlc",
+  "is-removable": true,
+  "loadpin-verity-digest": false,
+  "manifest-version": 1,
+  "mount-file-required": false,
+  "name": "Sample DLC",
+  "package": "package",
+  "pre-allocated-size": "4194304",
+  "preload-allowed": true,
+  "reserved": false,
+  "size": "%s",
+  "table-sha256-hash": )"
+    R"("44a4e688209bda4e06fd41aadc85a51de7d74a641275cb63b7caead96a9b03b7",
+  "used-by": "system",
+  "version": "1.0.0-r1"
+})";
+constexpr char kProperties[] = R"(
+CHROMEOS_RELEASE_APPID={DEB6CEFD-4EEE-462F-AC21-52DF1E17B52F}
+CHROMEOS_BOARD_APPID={DEB6CEFD-4EEE-462F-AC21-52DF1E17B52F}
+CHROMEOS_CANARY_APPID={90F229CE-83E2-4FAF-8479-E368A34938B1}
+DEVICETYPE=CHROMEBOOK
+CHROMEOS_RELEASE_NAME=Chrome OS
+CHROMEOS_AUSERVER=https://tools.google.com/service/update2
+CHROMEOS_DEVSERVER=
+CHROMEOS_ARC_VERSION=9196679
+CHROMEOS_ARC_ANDROID_SDK_VERSION=30
+CHROMEOS_RELEASE_BUILDER_PATH=brya-release/R109-15201.0.0
+CHROMEOS_RELEASE_KEYSET=devkeys
+CHROMEOS_RELEASE_TRACK=testimage-channel
+CHROMEOS_RELEASE_BUILD_TYPE=Official Build
+CHROMEOS_RELEASE_DESCRIPTION=15201.0.0 (Official Build) dev-channel brya test
+CHROMEOS_RELEASE_BOARD=brya
+CHROMEOS_RELEASE_BRANCH_NUMBER=0
+CHROMEOS_RELEASE_BUILD_NUMBER=15201
+CHROMEOS_RELEASE_CHROME_MILESTONE=109
+CHROMEOS_RELEASE_PATCH_NUMBER=0
+CHROMEOS_RELEASE_VERSION=15201.0.0
+GOOGLE_RELEASE=15201.0.0
+CHROMEOS_RELEASE_UNIBUILD=1
+)";
+
+class InstallActionTestProcessorDelegate : public ActionProcessorDelegate {
+ public:
+  InstallActionTestProcessorDelegate() : expected_code_(ErrorCode::kSuccess) {}
+  ~InstallActionTestProcessorDelegate() override = default;
+
+  void ProcessingDone(const ActionProcessor* processor,
+                      ErrorCode code) override {
+    brillo::MessageLoop::current()->BreakLoop();
+  }
+
+  void ActionCompleted(ActionProcessor* processor,
+                       AbstractAction* action,
+                       ErrorCode code) override {
+    EXPECT_EQ(InstallAction::StaticType(), action->Type());
+    EXPECT_EQ(expected_code_, code);
+  }
+
+  ErrorCode expected_code_{ErrorCode::kSuccess};
+};
+}  // namespace
+
+class InstallActionTest : public ::testing::Test {
+ protected:
+  InstallActionTest() : data_(1024) {}
+  ~InstallActionTest() override = default;
+
+  void SetUp() override {
+    loop_.SetAsCurrent();
+
+    ASSERT_TRUE(tempdir_.CreateUniqueTempDir());
+    EXPECT_TRUE(base::CreateDirectory(tempdir_.GetPath().Append("etc")));
+    EXPECT_TRUE(base::CreateDirectory(
+        tempdir_.GetPath().Append("dlc/foobar-dlc/package")));
+    test::SetImagePropertiesRootPrefix(tempdir_.GetPath().value().c_str());
+    FakeSystemState::CreateInstance();
+
+    auto http_fetcher =
+        std::make_unique<MockHttpFetcher>(data_.data(), data_.size(), nullptr);
+    install_action_ = std::make_unique<InstallAction>(
+        std::move(http_fetcher),
+        "foobar-dlc",
+        /*slotting=*/"",
+        /*manifest_dir=*/tempdir_.GetPath().Append("dlc").value());
+  }
+
+  base::ScopedTempDir tempdir_;
+
+  brillo::Blob data_;
+  std::unique_ptr<InstallAction> install_action_;
+
+  InstallActionTestProcessorDelegate delegate_;
+
+  ActionProcessor processor_;
+  brillo::FakeMessageLoop loop_{nullptr};
+};
+
+TEST_F(InstallActionTest, ManifestReadFailure) {
+  processor_.set_delegate(&delegate_);
+  processor_.EnqueueAction(std::move(install_action_));
+
+  ASSERT_TRUE(test_utils::WriteFileString(
+      tempdir_.GetPath()
+          .Append("dlc/foobar-dlc/package/imageloader.json")
+          .value(),
+      ""));
+  delegate_.expected_code_ = ErrorCode::kScaledInstallationError;
+
+  loop_.PostTask(
+      FROM_HERE,
+      base::Bind(
+          [](ActionProcessor* processor) { processor->StartProcessing(); },
+          base::Unretained(&processor_)));
+  loop_.Run();
+  EXPECT_FALSE(loop_.PendingTasks());
+}
+
+TEST_F(InstallActionTest, PerformSuccessfulTest) {
+  processor_.set_delegate(&delegate_);
+  processor_.EnqueueAction(std::move(install_action_));
+
+  auto manifest =
+      base::StringPrintf(kManifestTemplate, kDefaultSha, kDefaultOffset);
+  ASSERT_TRUE(test_utils::WriteFileString(
+      tempdir_.GetPath()
+          .Append("dlc/foobar-dlc/package/imageloader.json")
+          .value(),
+      manifest));
+  ASSERT_TRUE(test_utils::WriteFileString(
+      tempdir_.GetPath().Append("etc/lsb-release").value(), kProperties));
+  delegate_.expected_code_ = ErrorCode::kSuccess;
+
+  ASSERT_TRUE(test_utils::WriteFileString(
+      tempdir_.GetPath().Append("foobar-dlc-device").value(), ""));
+  FakeSystemState::Get()->fake_boot_control()->SetPartitionDevice(
+      "dlc/foobar-dlc/package",
+      0,
+      tempdir_.GetPath().Append("foobar-dlc-device").value());
+
+  loop_.PostTask(
+      FROM_HERE,
+      base::Bind(
+          [](ActionProcessor* processor) { processor->StartProcessing(); },
+          base::Unretained(&processor_)));
+  loop_.Run();
+  EXPECT_FALSE(loop_.PendingTasks());
+}
+
+// This also tests backup URLs.
+TEST_F(InstallActionTest, PerformInvalidOffsetTest) {
+  processor_.set_delegate(&delegate_);
+  processor_.EnqueueAction(std::move(install_action_));
+
+  auto manifest = base::StringPrintf(kManifestTemplate, kDefaultSha, "1025");
+  ASSERT_TRUE(test_utils::WriteFileString(
+      tempdir_.GetPath()
+          .Append("dlc/foobar-dlc/package/imageloader.json")
+          .value(),
+      manifest));
+  ASSERT_TRUE(test_utils::WriteFileString(
+      tempdir_.GetPath().Append("etc/lsb-release").value(), kProperties));
+  delegate_.expected_code_ = ErrorCode::kScaledInstallationError;
+
+  ASSERT_TRUE(test_utils::WriteFileString(
+      tempdir_.GetPath().Append("foobar-dlc-device").value(), ""));
+  FakeSystemState::Get()->fake_boot_control()->SetPartitionDevice(
+      "dlc/foobar-dlc/package",
+      0,
+      tempdir_.GetPath().Append("foobar-dlc-device").value());
+
+  loop_.PostTask(
+      FROM_HERE,
+      base::Bind(
+          [](ActionProcessor* processor) { processor->StartProcessing(); },
+          base::Unretained(&processor_)));
+  loop_.Run();
+  EXPECT_FALSE(loop_.PendingTasks());
+}
+
+// This also tests backup URLs.
+TEST_F(InstallActionTest, PerformInvalidShaTest) {
+  processor_.set_delegate(&delegate_);
+  processor_.EnqueueAction(std::move(install_action_));
+
+  auto manifest = base::StringPrintf(
+      kManifestTemplate,
+      "5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10deadbeef",
+      kDefaultOffset);
+  ASSERT_TRUE(test_utils::WriteFileString(
+      tempdir_.GetPath()
+          .Append("dlc/foobar-dlc/package/imageloader.json")
+          .value(),
+      manifest));
+  ASSERT_TRUE(test_utils::WriteFileString(
+      tempdir_.GetPath().Append("etc/lsb-release").value(), kProperties));
+  delegate_.expected_code_ = ErrorCode::kScaledInstallationError;
+
+  ASSERT_TRUE(test_utils::WriteFileString(
+      tempdir_.GetPath().Append("foobar-dlc-device").value(), ""));
+  FakeSystemState::Get()->fake_boot_control()->SetPartitionDevice(
+      "dlc/foobar-dlc/package",
+      0,
+      tempdir_.GetPath().Append("foobar-dlc-device").value());
+
+  loop_.PostTask(
+      FROM_HERE,
+      base::Bind(
+          [](ActionProcessor* processor) { processor->StartProcessing(); },
+          base::Unretained(&processor_)));
+  loop_.Run();
+  EXPECT_FALSE(loop_.PendingTasks());
+}
+
+}  // namespace chromeos_update_engine
diff --git a/cros/mock_update_attempter.h b/cros/mock_update_attempter.h
index 2146194..55d93f6 100644
--- a/cros/mock_update_attempter.h
+++ b/cros/mock_update_attempter.h
@@ -50,9 +50,10 @@
               (const update_engine::UpdateParams&),
               (override));
 
-  MOCK_METHOD2(CheckForInstall,
+  MOCK_METHOD3(CheckForInstall,
                bool(const std::vector<std::string>& dlc_ids,
-                    const std::string& omaha_url));
+                    const std::string& omaha_url,
+                    bool scaled));
 
   MOCK_METHOD2(SetDlcActiveValue, bool(bool, const std::string&));
 
diff --git a/cros/payload_state.cc b/cros/payload_state.cc
index 66a3b98..007bef8 100644
--- a/cros/payload_state.cc
+++ b/cros/payload_state.cc
@@ -379,6 +379,7 @@
     case ErrorCode::kRepeatedFpFromOmahaError:
     case ErrorCode::kInvalidateLastUpdate:
     case ErrorCode::kOmahaUpdateIgnoredOverMetered:
+    case ErrorCode::kScaledInstallationError:
       LOG(INFO) << "Not incrementing URL index or failure count for this error";
       break;
 
diff --git a/cros/update_attempter.cc b/cros/update_attempter.cc
index 5574e2b..3fd5905 100644
--- a/cros/update_attempter.cc
+++ b/cros/update_attempter.cc
@@ -55,6 +55,7 @@
 #include "update_engine/common/system_state.h"
 #include "update_engine/common/utils.h"
 #include "update_engine/cros/download_action_chromeos.h"
+#include "update_engine/cros/install_action.h"
 #include "update_engine/cros/omaha_request_action.h"
 #include "update_engine/cros/omaha_request_params.h"
 #include "update_engine/cros/omaha_response_handler_action.h"
@@ -107,6 +108,18 @@
 // different params are passed to CheckForUpdate().
 const char kAUTestURLRequest[] = "autest";
 const char kScheduledAUTestURLRequest[] = "autest-scheduled";
+
+string ConvertToString(ProcessMode op) {
+  switch (op) {
+    case ProcessMode::UPDATE:
+      return "update";
+    case ProcessMode::INSTALL:
+      return "install";
+    case ProcessMode::SCALED_INSTALL:
+      return "scaled install";
+  }
+}
+
 }  // namespace
 
 ErrorCode GetErrorCodeForAction(AbstractAction* action, ErrorCode code) {
@@ -129,7 +142,6 @@
 UpdateAttempter::UpdateAttempter(CertificateChecker* cert_checker)
     : processor_(new ActionProcessor()),
       cert_checker_(cert_checker),
-      is_install_(false),
       weak_ptr_factory_(this) {}
 
 UpdateAttempter::~UpdateAttempter() {
@@ -174,7 +186,7 @@
 }
 
 bool UpdateAttempter::IsUpdating() {
-  return !is_install_;
+  return pm_ == ProcessMode::UPDATE;
 }
 
 bool UpdateAttempter::ScheduleUpdates() {
@@ -355,6 +367,30 @@
   ScheduleProcessingStart();
 }
 
+void UpdateAttempter::Install() {
+  CHECK(!processor_->IsRunning());
+  processor_->set_delegate(this);
+
+  if (dlc_ids_.size() != 1) {
+    LOG(ERROR) << "Could not kick off installation.";
+    return;
+  }
+  const auto& dlc_id = dlc_ids_[0];
+
+  auto http_fetcher = std::make_unique<LibcurlHttpFetcher>(
+      GetProxyResolver(), SystemState::Get()->hardware());
+  auto install_action = std::make_unique<InstallAction>(
+      std::move(http_fetcher), dlc_id, /*slotting=*/"");
+  install_action->set_delegate(this);
+  SetOutPipe(install_action.get());
+  processor_->EnqueueAction(std::move(install_action));
+
+  // Simply go into CHECKING status.
+  SetStatusAndNotify(UpdateStatus::CHECKING_FOR_UPDATE);
+
+  ScheduleProcessingStart();
+}
+
 void UpdateAttempter::RefreshDevicePolicy() {
   // Lazy initialize the policy provider, or reload the latest policy data.
   if (!policy_provider_.get())
@@ -728,7 +764,7 @@
 void UpdateAttempter::CalculateDlcParams() {
   // Set the |dlc_ids_| only for an update. This is required to get the
   // currently installed DLC(s).
-  if (!is_install_ &&
+  if (IsUpdating() &&
       !SystemState::Get()->dlcservice()->GetDlcsToUpdate(&dlc_ids_)) {
     LOG(INFO) << "Failed to retrieve DLC module IDs from dlcservice. Check the "
                  "state of dlcservice, will not update DLC modules.";
@@ -739,7 +775,7 @@
         .active_counting_type = OmahaRequestParams::kDateBased,
         .name = dlc_id,
         .send_ping = false};
-    if (is_install_) {
+    if (!IsUpdating()) {
       // In some cases, |SetDlcActiveValue| might fail to reset the DLC prefs
       // when a DLC is uninstalled. To avoid having stale values from that
       // scenario, we reset the metadata values on a new install request.
@@ -775,7 +811,7 @@
     dlc_apps_params[omaha_request_params_->GetDlcAppId(dlc_id)] = dlc_params;
   }
   omaha_request_params_->set_dlc_apps_params(dlc_apps_params);
-  omaha_request_params_->set_is_install(is_install_);
+  omaha_request_params_->set_is_install(!IsUpdating());
 }
 
 void UpdateAttempter::BuildUpdateActions(bool interactive) {
@@ -864,7 +900,7 @@
 }
 
 bool UpdateAttempter::Rollback(bool powerwash) {
-  is_install_ = false;
+  pm_ = ProcessMode::UPDATE;
   if (!CanRollback()) {
     return false;
   }
@@ -963,14 +999,13 @@
   if (status_ != UpdateStatus::IDLE &&
       status_ != UpdateStatus::UPDATED_NEED_REBOOT) {
     LOG(INFO) << "Refusing to do an update as there is an "
-              << (is_install_ ? "install" : "update")
-              << " already in progress.";
+              << ConvertToString(pm_) << " already in progress.";
     return false;
   }
 
   const auto& update_flags = update_params.update_flags();
   bool interactive = !update_flags.non_interactive();
-  is_install_ = false;
+  pm_ = ProcessMode::UPDATE;
   if (update_params.skip_applying()) {
     skip_applying_ = true;
     LOG(INFO) << "Update check is only going to query server for update, will "
@@ -1076,16 +1111,24 @@
 }
 
 bool UpdateAttempter::CheckForInstall(const vector<string>& dlc_ids,
-                                      const string& omaha_url) {
+                                      const string& omaha_url,
+                                      bool scaled) {
   if (status_ != UpdateStatus::IDLE) {
     LOG(INFO) << "Refusing to do an install as there is an "
-              << (is_install_ ? "install" : "update")
-              << " already in progress.";
+              << ConvertToString(pm_) << " already in progress.";
     return false;
   }
 
   dlc_ids_ = dlc_ids;
-  is_install_ = true;
+  pm_ = ProcessMode::INSTALL;
+  if (scaled) {
+    pm_ = ProcessMode::SCALED_INSTALL;
+    if (dlc_ids_.size() != 1) {
+      LOG(ERROR) << "Can't install more than one scaled DLC at a time.";
+      return false;
+    }
+  }
+
   forced_omaha_url_.clear();
 
   // Certain conditions must be met to allow setting custom version and update
@@ -1170,7 +1213,7 @@
     }
 
     LOG(INFO) << "Running " << (params.interactive ? "interactive" : "periodic")
-              << " update.";
+              << " " << ConvertToString(pm_);
 
     if (!params.interactive) {
       // Cache the update attempt flags that will be used by this update attempt
@@ -1178,7 +1221,15 @@
       current_update_flags_ = update_flags_;
     }
 
-    Update(params);
+    switch (pm_) {
+      case ProcessMode::UPDATE:
+      case ProcessMode::INSTALL:
+        Update(params);
+        break;
+      case ProcessMode::SCALED_INSTALL:
+        Install();
+        break;
+    }
     // Always clear the forced app_version and omaha_url after an update attempt
     // so the next update uses the defaults.
     forced_app_version_.clear();
@@ -1277,10 +1328,14 @@
   prefs_->Delete(kPrefsUpdateFirstSeenAt);
 
   // Note: below this comment should only be on |ErrorCode::kSuccess|.
-  if (is_install_) {
-    ProcessingDoneInstall(processor, code);
-  } else {
-    ProcessingDoneUpdate(processor, code);
+  switch (pm_) {
+    case ProcessMode::UPDATE:
+      ProcessingDoneUpdate(processor, code);
+      break;
+    case ProcessMode::INSTALL:
+    case ProcessMode::SCALED_INSTALL:
+      ProcessingDoneInstall(processor, code);
+      break;
   }
 }
 
@@ -1395,7 +1450,7 @@
 
   // Note: do cleanups here for any variables that need to be reset after a
   // failure, error, update, or install.
-  is_install_ = false;
+  pm_ = ProcessMode::UPDATE;
   skip_applying_ = false;
 }
 
@@ -1491,7 +1546,15 @@
       cpu_limiter_.StartLimiter();
       SetStatusAndNotify(UpdateStatus::UPDATE_AVAILABLE);
     }
+  } else if (type == InstallAction::StaticType()) {
+    // TODO(b/236008158): Report metrics here.
+    if (code == ErrorCode::kSuccess) {
+      LOG(INFO) << "InstallAction succeeded.";
+    } else {
+      LOG(INFO) << "InstallAction failed.";
+    }
   }
+
   // General failure cases.
   if (code != ErrorCode::kSuccess) {
     // Best effort to invalidate the previous update by resetting the active
@@ -1557,13 +1620,7 @@
   }
 }
 
-void UpdateAttempter::BytesReceived(uint64_t bytes_progressed,
-                                    uint64_t bytes_received,
-                                    uint64_t total) {
-  // The PayloadState keeps track of how many bytes were actually downloaded
-  // from a given URL for the URL skipping logic.
-  SystemState::Get()->payload_state()->DownloadProgress(bytes_progressed);
-
+void UpdateAttempter::ProgressUpdate(uint64_t bytes_received, uint64_t total) {
   double progress = 0;
   if (total)
     progress = static_cast<double>(bytes_received) / static_cast<double>(total);
@@ -1575,6 +1632,19 @@
   }
 }
 
+void UpdateAttempter::BytesReceived(uint64_t bytes_progressed,
+                                    uint64_t bytes_received,
+                                    uint64_t total) {
+  // The PayloadState keeps track of how many bytes were actually downloaded
+  // from a given URL for the URL skipping logic.
+  SystemState::Get()->payload_state()->DownloadProgress(bytes_progressed);
+  ProgressUpdate(bytes_received, total);
+}
+
+void UpdateAttempter::BytesReceived(uint64_t bytes_received, uint64_t total) {
+  ProgressUpdate(bytes_received, total);
+}
+
 void UpdateAttempter::ResetUpdateStatus() {
   // If `GetBootTimeAtUpdate` is true, then the update complete markers exist
   // and there is an update in the inactive partition waiting to be applied.
@@ -1737,7 +1807,8 @@
   out_status->new_version = new_version_;
   out_status->is_enterprise_rollback =
       install_plan_ && install_plan_->is_rollback;
-  out_status->is_install = is_install_;
+  out_status->is_install =
+      (pm_ == ProcessMode::INSTALL || pm_ == ProcessMode::SCALED_INSTALL);
   out_status->update_urgency_internal =
       install_plan_ ? install_plan_->update_urgency
                     : update_engine::UpdateUrgencyInternal::REGULAR;
diff --git a/cros/update_attempter.h b/cros/update_attempter.h
index 53ba9de..dae85bd 100644
--- a/cros/update_attempter.h
+++ b/cros/update_attempter.h
@@ -41,6 +41,7 @@
 #include "update_engine/common/service_observer_interface.h"
 #include "update_engine/common/system_state.h"
 #include "update_engine/cros/chrome_browser_proxy_resolver.h"
+#include "update_engine/cros/install_action.h"
 #include "update_engine/cros/omaha_request_builder_xml.h"
 #include "update_engine/cros/omaha_request_params.h"
 #include "update_engine/cros/omaha_response_handler_action.h"
@@ -56,9 +57,17 @@
 
 namespace chromeos_update_engine {
 
+// The different types of top level operations that are processed through.
+enum class ProcessMode {
+  UPDATE,
+  INSTALL,
+  SCALED_INSTALL,
+};
+
 class UpdateAttempter : public ActionProcessorDelegate,
                         public DownloadActionDelegate,
                         public CertificateChecker::Observer,
+                        public InstallActionDelegate,
                         public PostinstallRunnerAction::DelegateInterface,
                         public DaemonStateInterface {
  public:
@@ -85,6 +94,9 @@
   // the system.
   virtual void Update(const chromeos_update_manager::UpdateCheckParams& params);
 
+  // Performs a scaled install of a DLC.
+  virtual void Install();
+
   // ActionProcessorDelegate methods:
   void ProcessingDone(const ActionProcessor* processor,
                       ErrorCode code) override;
@@ -150,7 +162,8 @@
 
   // This is the version of CheckForUpdate called by AttemptInstall API.
   virtual bool CheckForInstall(const std::vector<std::string>& dlc_ids,
-                               const std::string& omaha_url);
+                               const std::string& omaha_url,
+                               bool scaled = false);
 
   // This is the internal entry point for going through a rollback. This will
   // attempt to run the postinstall on the non-active partition and set it as
@@ -177,11 +190,17 @@
   // Sets the DLC as active or inactive. See chromeos/common_service.h
   virtual bool SetDlcActiveValue(bool is_active, const std::string& dlc_id);
 
+  // Broadcasts the download/install progress.
+  void ProgressUpdate(uint64_t bytes_received, uint64_t total);
+
   // DownloadActionDelegate methods:
   void BytesReceived(uint64_t bytes_progressed,
                      uint64_t bytes_received,
                      uint64_t total) override;
 
+  // InstallActionDelegate methods:
+  void BytesReceived(uint64_t bytes_received, uint64_t total) override;
+
   // Returns that the update should be canceled when the download channel was
   // changed.
   bool ShouldCancel(ErrorCode* cancel_reason) override;
@@ -317,6 +336,7 @@
   FRIEND_TEST(UpdateAttempterTest, ReportDailyMetrics);
   FRIEND_TEST(UpdateAttempterTest, RollbackNotAllowed);
   FRIEND_TEST(UpdateAttempterTest, RollbackAfterInstall);
+  FRIEND_TEST(UpdateAttempterTest, RollbackAfterScaledInstall);
   FRIEND_TEST(UpdateAttempterTest, RollbackAllowed);
   FRIEND_TEST(UpdateAttempterTest, RollbackAllowedSetAndReset);
   FRIEND_TEST(UpdateAttempterTest, ChannelDowngradeNoRollback);
@@ -334,6 +354,7 @@
   FRIEND_TEST(UpdateAttempterTest, TargetChannelHintSetAndReset);
   FRIEND_TEST(UpdateAttempterTest, TargetVersionPrefixSetAndReset);
   FRIEND_TEST(UpdateAttempterTest, UpdateAfterInstall);
+  FRIEND_TEST(UpdateAttempterTest, UpdateAfterScaledInstall);
   FRIEND_TEST(UpdateAttempterTest, UpdateFlagsCachedAtUpdateStart);
   FRIEND_TEST(UpdateAttempterTest, UpdateDeferredByPolicyTest);
   FRIEND_TEST(UpdateAttempterTest, UpdateIsNotRunningWhenUpdateAvailable);
@@ -347,6 +368,9 @@
   FRIEND_TEST(UpdateAttempterTest, ConsecutiveUpdateFailureMetric);
   FRIEND_TEST(UpdateAttempterTest, ResetUpdatePrefs);
   FRIEND_TEST(UpdateAttempterTest, ProcessingDoneSkipApplying);
+  FRIEND_TEST(UpdateAttempterTest, InstallZeroDlcTest);
+  FRIEND_TEST(UpdateAttempterTest, InstallSingleDlcTest);
+  FRIEND_TEST(UpdateAttempterTest, InstallMultiDlcTest);
 
   // Returns the special flags to be added to ErrorCode values based on the
   // parameters used in the current update attempt.
@@ -614,9 +638,9 @@
 
   // A list of DLC module IDs.
   std::vector<std::string> dlc_ids_;
-  // Whether the operation is install (write to the current slot not the
-  // inactive slot).
-  bool is_install_;
+
+  // What type of operation is happening/scheduled.
+  ProcessMode pm_{ProcessMode::UPDATE};
 
   // If this is not TimeDelta(), then that means staging is turned on.
   base::TimeDelta staging_wait_time_;
diff --git a/cros/update_attempter_unittest.cc b/cros/update_attempter_unittest.cc
index aa5e57f..ed7dc5f 100644
--- a/cros/update_attempter_unittest.cc
+++ b/cros/update_attempter_unittest.cc
@@ -139,7 +139,7 @@
 
 struct ProcessingDoneTestParams {
   // Setups + Inputs:
-  bool is_install = false;
+  ProcessMode pm = ProcessMode::UPDATE;
   UpdateStatus status = UpdateStatus::CHECKING_FOR_UPDATE;
   ActionProcessor* processor = nullptr;
   ErrorCode code = ErrorCode::kSuccess;
@@ -147,7 +147,7 @@
   bool skip_applying = false;
 
   // Expects:
-  const bool kExpectedIsInstall = false;
+  const ProcessMode kExpectedProcessMode = ProcessMode::UPDATE;
   bool should_schedule_updates_be_called = true;
   UpdateStatus expected_exit_status = UpdateStatus::IDLE;
   bool should_install_completed_be_called = false;
@@ -375,7 +375,7 @@
 void UpdateAttempterTest::TestProcessingDone() {
   // Setup
   attempter_.DisableScheduleUpdates();
-  attempter_.is_install_ = pd_params_.is_install;
+  attempter_.pm_ = pd_params_.pm;
   attempter_.status_ = pd_params_.status;
   attempter_.omaha_request_params_->set_dlc_apps_params(
       pd_params_.dlc_apps_params);
@@ -399,7 +399,7 @@
   attempter_.ProcessingDone(pd_params_.processor, pd_params_.code);
 
   // Verify
-  EXPECT_EQ(pd_params_.kExpectedIsInstall, attempter_.is_install_);
+  EXPECT_EQ(pd_params_.kExpectedProcessMode, attempter_.pm_);
   EXPECT_EQ(pd_params_.should_schedule_updates_be_called,
             attempter_.WasScheduleUpdatesCalled());
   EXPECT_EQ(pd_params_.expected_exit_status, attempter_.status_);
@@ -1651,10 +1651,22 @@
   EXPECT_EQ("", attempter_.forced_omaha_url());
 }
 
+TEST_F(UpdateAttempterTest, CheckForInstallScaledTest) {
+  FakeSystemState::Get()->fake_hardware()->SetIsOfficialBuild(true);
+  FakeSystemState::Get()->fake_hardware()->SetAreDevFeaturesEnabled(false);
+  EXPECT_FALSE(attempter_.CheckForInstall({}, "autest", /*scaled=*/true));
+
+  EXPECT_TRUE(attempter_.CheckForInstall({"dlc_a"}, "autest", /*scaled=*/true));
+  EXPECT_EQ(constants::kOmahaDefaultAUTestURL, attempter_.forced_omaha_url());
+
+  EXPECT_FALSE(attempter_.CheckForInstall(
+      {"dlc_a", "dlc_b"}, "autest", /*scaled=*/true));
+}
+
 TEST_F(UpdateAttempterTest, InstallSetsStatusIdle) {
   attempter_.CheckForInstall({}, "http://foo.bar");
   attempter_.status_ = UpdateStatus::DOWNLOADING;
-  EXPECT_TRUE(attempter_.is_install_);
+  EXPECT_FALSE(attempter_.IsUpdating());
   attempter_.ProcessingDone(nullptr, ErrorCode::kSuccess);
   UpdateEngineStatus status;
   attempter_.GetStatus(&status);
@@ -1663,15 +1675,27 @@
 }
 
 TEST_F(UpdateAttempterTest, RollbackAfterInstall) {
-  attempter_.is_install_ = true;
+  attempter_.pm_ = ProcessMode::INSTALL;
   attempter_.Rollback(false);
-  EXPECT_FALSE(attempter_.is_install_);
+  EXPECT_TRUE(attempter_.IsUpdating());
+}
+
+TEST_F(UpdateAttempterTest, RollbackAfterScaledInstall) {
+  attempter_.pm_ = ProcessMode::SCALED_INSTALL;
+  attempter_.Rollback(false);
+  EXPECT_TRUE(attempter_.IsUpdating());
 }
 
 TEST_F(UpdateAttempterTest, UpdateAfterInstall) {
-  attempter_.is_install_ = true;
+  attempter_.pm_ = ProcessMode::INSTALL;
   attempter_.CheckForUpdate({});
-  EXPECT_FALSE(attempter_.is_install_);
+  EXPECT_TRUE(attempter_.IsUpdating());
+}
+
+TEST_F(UpdateAttempterTest, UpdateAfterScaledInstall) {
+  attempter_.pm_ = ProcessMode::SCALED_INSTALL;
+  attempter_.CheckForUpdate({});
+  EXPECT_TRUE(attempter_.IsUpdating());
 }
 
 TEST_F(UpdateAttempterTest, TargetVersionPrefixSetAndReset) {
@@ -2006,7 +2030,7 @@
 
 TEST_F(UpdateAttempterTest, ProcessingDoneInstalled) {
   // GIVEN an install finished.
-  pd_params_.is_install = true;
+  pd_params_.pm = ProcessMode::INSTALL;
 
   // THEN update_engine should call install completion.
   pd_params_.should_install_completed_be_called = true;
@@ -2018,7 +2042,7 @@
 
 TEST_F(UpdateAttempterTest, ProcessingDoneInstalledDlcFilter) {
   // GIVEN an install finished.
-  pd_params_.is_install = true;
+  pd_params_.pm = ProcessMode::INSTALL;
   // GIVEN DLC |AppParams| list.
   auto dlc_1 = "dlc_1", dlc_2 = "dlc_2";
   pd_params_.dlc_apps_params = {{dlc_1, {.name = dlc_1, .updated = false}},
@@ -2035,7 +2059,7 @@
 
 TEST_F(UpdateAttempterTest, ProcessingDoneInstallReportingError) {
   // GIVEN an install finished.
-  pd_params_.is_install = true;
+  pd_params_.pm = ProcessMode::INSTALL;
   // GIVEN a reporting error occurred.
   pd_params_.status = UpdateStatus::REPORTING_ERROR_EVENT;
 
@@ -2060,7 +2084,7 @@
 
 TEST_F(UpdateAttempterTest, ProcessingDoneNoInstall) {
   // GIVEN an install finished.
-  pd_params_.is_install = true;
+  pd_params_.pm = ProcessMode::INSTALL;
   // GIVEN an action error occured.
   pd_params_.code = ErrorCode::kNoUpdate;
 
@@ -2123,7 +2147,7 @@
 
 TEST_F(UpdateAttempterTest, ProcessingDoneInstallError) {
   // GIVEN an install finished.
-  pd_params_.is_install = true;
+  pd_params_.pm = ProcessMode::INSTALL;
   // GIVEN an action error occured.
   pd_params_.code = ErrorCode::kError;
   // GIVEN an event error is set.
@@ -2422,7 +2446,7 @@
 
 TEST_F(UpdateAttempterTest, CalculateDlcParamsInstallTest) {
   string dlc_id = "dlc0";
-  attempter_.is_install_ = true;
+  attempter_.pm_ = ProcessMode::INSTALL;
   attempter_.dlc_ids_ = {dlc_id};
   attempter_.CalculateDlcParams();
 
@@ -2448,7 +2472,7 @@
       .WillOnce(
           DoAll(SetArgPointee<0>(std::vector<string>({dlc_id})), Return(true)));
 
-  attempter_.is_install_ = false;
+  attempter_.pm_ = ProcessMode::UPDATE;
   attempter_.CalculateDlcParams();
 
   OmahaRequestParams* params = FakeSystemState::Get()->request_params();
@@ -2482,7 +2506,7 @@
   FakeSystemState::Get()->prefs()->SetString(active_key, "z2yz");
   FakeSystemState::Get()->prefs()->SetString(last_active_key, "z2yz");
   FakeSystemState::Get()->prefs()->SetString(last_rollcall_key, "z2yz");
-  attempter_.is_install_ = false;
+  attempter_.pm_ = ProcessMode::UPDATE;
   attempter_.CalculateDlcParams();
 
   OmahaRequestParams* params = FakeSystemState::Get()->request_params();
@@ -2517,7 +2541,7 @@
   FakeSystemState::Get()->prefs()->SetInt64(last_active_key, 78);
   FakeSystemState::Get()->prefs()->SetInt64(last_rollcall_key, 99);
   FakeSystemState::Get()->prefs()->SetString(last_fp_key, "3.75");
-  attempter_.is_install_ = false;
+  attempter_.pm_ = ProcessMode::UPDATE;
   attempter_.CalculateDlcParams();
 
   OmahaRequestParams* params = FakeSystemState::Get()->request_params();
@@ -2535,7 +2559,7 @@
 
 TEST_F(UpdateAttempterTest, ConsecutiveUpdateBeforeRebootSuccess) {
   FakeSystemState::Get()->prefs()->SetString(kPrefsLastFp, "3.75");
-  attempter_.is_install_ = false;
+  attempter_.pm_ = ProcessMode::UPDATE;
   attempter_.install_plan_.reset(new InstallPlan);
   attempter_.install_plan_->payloads.push_back(
       {.size = 1234ULL, .type = InstallPayloadType::kFull, .fp = "4.0"});
@@ -2620,7 +2644,7 @@
   EXPECT_TRUE(FakeSystemState::Get()->prefs()->Exists(last_rollcall_key));
 
   attempter_.dlc_ids_ = {dlc_id};
-  attempter_.is_install_ = true;
+  attempter_.pm_ = ProcessMode::INSTALL;
   attempter_.CalculateDlcParams();
 
   EXPECT_FALSE(FakeSystemState::Get()->prefs()->Exists(last_active_key));
@@ -2715,4 +2739,23 @@
   EXPECT_FALSE(fake_prefs->Exists(kPrefsLastFp));
   EXPECT_FALSE(fake_prefs->Exists(kPrefsPreviousVersion));
 }
+
+TEST_F(UpdateAttempterTest, InstallZeroDlcTest) {
+  attempter_.Install();
+  EXPECT_EQ(UpdateStatus::IDLE, attempter_.status_);
+}
+
+TEST_F(UpdateAttempterTest, InstallSingleDlcTest) {
+  attempter_.dlc_ids_ = {"dlc_a"};
+  attempter_.Install();
+  EXPECT_EQ(UpdateStatus::CHECKING_FOR_UPDATE, attempter_.status_);
+  loop_.BreakLoop();
+}
+
+TEST_F(UpdateAttempterTest, InstallMultiDlcTest) {
+  attempter_.dlc_ids_ = {"dlc_a", "dlc_b"};
+  attempter_.Install();
+  EXPECT_EQ(UpdateStatus::IDLE, attempter_.status_);
+}
+
 }  // namespace chromeos_update_engine
diff --git a/cros/update_engine_client.cc b/cros/update_engine_client.cc
index 009dce9..fc8b2d9 100644
--- a/cros/update_engine_client.cc
+++ b/cros/update_engine_client.cc
@@ -269,6 +269,7 @@
               "Wait for any update operations to complete."
               "Exit status is 0 if the update succeeded, and 1 otherwise.");
   DEFINE_bool(install, false, "Set to perform an installation.");
+  DEFINE_bool(scaled, false, "Set to perform a scaled installation.");
   DEFINE_bool(interactive, true, "Mark the update request as interactive.");
   DEFINE_string(omaha_url, "", "The URL of the Omaha update server.");
   DEFINE_string(p2p_update,
@@ -534,11 +535,19 @@
       LOG(ERROR) << "Must pass in a DLC when performing an install.";
       return 1;
     }
-    if (!client_->AttemptInstall(FLAGS_omaha_url, {FLAGS_dlc})) {
+
+    update_engine::InstallParams install_params;
+    install_params.set_id(FLAGS_dlc);
+    install_params.set_omaha_url(FLAGS_omaha_url);
+    install_params.set_scaled(FLAGS_scaled);
+
+    if (!client_->Install(install_params)) {
       LOG(ERROR) << "Failed to install DLC=" << FLAGS_dlc;
       return 1;
     }
+
     LOG(INFO) << "Waiting for install to complete.";
+
     auto handler = new InstallWaitHandler(client_.get());
     handlers_.emplace_back(handler);
     client_->RegisterStatusUpdateHandler(handler);
diff --git a/dbus_bindings/org.chromium.UpdateEngineInterface.dbus-xml b/dbus_bindings/org.chromium.UpdateEngineInterface.dbus-xml
index fac9cc8..db5f046 100644
--- a/dbus_bindings/org.chromium.UpdateEngineInterface.dbus-xml
+++ b/dbus_bindings/org.chromium.UpdateEngineInterface.dbus-xml
@@ -41,6 +41,7 @@
                     value="update_engine::ApplyUpdateConfig"/>
       </arg>
     </method>
+    <!-- TODO(b/219067273): DEPRECATE -->
     <method name="AttemptInstall">
       <arg type="s" name="omaha_url" direction="in" />
       <arg type="as" name="dlc_ids" direction="in">
@@ -49,6 +50,15 @@
         </tp:docstring>
       </arg>
     </method>
+    <method name="Install">
+      <arg type="ay" name="install_params" direction="in">
+        <tp:docstring>
+          The install parameters for DLC.
+        </tp:docstring>
+        <annotation name="org.chromium.DBus.Argument.ProtobufClass"
+                    value="update_engine::InstallParams"/>
+      </arg>
+    </method>
     <method name="AttemptRollback">
       <arg type="b" name="powerwash" direction="in" />
     </method>
diff --git a/metrics_utils.cc b/metrics_utils.cc
index 8972f42..7b21afa 100644
--- a/metrics_utils.cc
+++ b/metrics_utils.cc
@@ -123,6 +123,7 @@
     case ErrorCode::kPackageExcludedFromUpdate:
     case ErrorCode::kInvalidateLastUpdate:
     case ErrorCode::kOmahaUpdateIgnoredOverMetered:
+    case ErrorCode::kScaledInstallationError:
       return metrics::AttemptResult::kInternalError;
 
     case ErrorCode::kOmahaUpdateDeferredPerPolicy:
@@ -250,6 +251,7 @@
     case ErrorCode::kRepeatedFpFromOmahaError:
     case ErrorCode::kInvalidateLastUpdate:
     case ErrorCode::kOmahaUpdateIgnoredOverMetered:
+    case ErrorCode::kScaledInstallationError:
       break;
 
     // Special flags. These can't happen (we mask them out above) but
diff --git a/update_manager/update_can_start_policy.cc b/update_manager/update_can_start_policy.cc
index 48366ed..1fd62b4 100644
--- a/update_manager/update_can_start_policy.cc
+++ b/update_manager/update_can_start_policy.cc
@@ -145,6 +145,7 @@
     case ErrorCode::kRepeatedFpFromOmahaError:
     case ErrorCode::kInvalidateLastUpdate:
     case ErrorCode::kOmahaUpdateIgnoredOverMetered:
+    case ErrorCode::kScaledInstallationError:
       LOG(INFO) << "Not changing URL index or failure count due to error "
                 << chromeos_update_engine::utils::ErrorCodeToString(err_code)
                 << " (" << static_cast<int>(err_code) << ")";