Implement Rollback to previously booted partitions.

This CL implements rollback to whatever partition we ran from before.
We expose this functionality via dbus under AttemptRollback and expose
a new command-line option to update_engine_client that a developer can
use.

BUG=chromium:242665
TEST=Unittests, full update, update + rollback and verified.

Change-Id: Ie59f90b9a0b777dc1329592449090c70892236bf
Reviewed-on: https://gerrit.chromium.org/gerrit/58427
Commit-Queue: Chris Sosa <sosa@chromium.org>
Reviewed-by: Chris Sosa <sosa@chromium.org>
Tested-by: Chris Sosa <sosa@chromium.org>
diff --git a/UpdateEngine.conf b/UpdateEngine.conf
index 4c6cd1c..c83d6f2 100644
--- a/UpdateEngine.conf
+++ b/UpdateEngine.conf
@@ -18,6 +18,9 @@
            send_member="AttemptUpdate"/>
     <allow send_destination="org.chromium.UpdateEngine"
            send_interface="org.chromium.UpdateEngineInterface"
+           send_member="AttemptRollback"/>
+    <allow send_destination="org.chromium.UpdateEngine"
+           send_interface="org.chromium.UpdateEngineInterface"
            send_member="ResetStatus"/>
     <allow send_destination="org.chromium.UpdateEngine"
            send_interface="org.chromium.UpdateEngineInterface"
diff --git a/dbus_service.cc b/dbus_service.cc
index 97aae35..f90b90e 100644
--- a/dbus_service.cc
+++ b/dbus_service.cc
@@ -101,6 +101,14 @@
   return TRUE;
 }
 
+gboolean update_engine_service_attempt_rollback(UpdateEngineService* self,
+                                                bool powerwash,
+                                                GError **error) {
+  LOG(INFO) << "Attempting rollback to non-active partitions.";
+  self->system_state_->update_attempter()->Rollback(powerwash);
+  return TRUE;
+}
+
 gboolean update_engine_service_reset_status(UpdateEngineService* self,
                                             GError **error) {
   *error = NULL;
diff --git a/dbus_service.h b/dbus_service.h
index 71e7997..d5e0240 100644
--- a/dbus_service.h
+++ b/dbus_service.h
@@ -51,6 +51,10 @@
                                               gchar* omaha_url,
                                               GError **error);
 
+gboolean update_engine_service_attempt_rollback(UpdateEngineService* self,
+                                                bool powerwash,
+                                                GError **error);
+
 gboolean update_engine_service_reset_status(UpdateEngineService* self,
                                             GError **error);
 
diff --git a/download_action.h b/download_action.h
index 318ece8..020aa9c 100644
--- a/download_action.h
+++ b/download_action.h
@@ -41,19 +41,9 @@
   virtual void BytesReceived(uint64_t bytes_received, uint64_t total) = 0;
 };
 
-class DownloadAction;
-class NoneType;
 class PrefsInterface;
 
-template<>
-class ActionTraits<DownloadAction> {
- public:
-  // Takes and returns an InstallPlan
-  typedef InstallPlan InputObjectType;
-  typedef InstallPlan OutputObjectType;
-};
-
-class DownloadAction : public Action<DownloadAction>,
+class DownloadAction : public InstallPlanAction,
                        public HttpFetcherDelegate {
  public:
   // Takes ownership of the passed in HttpFetcher. Useful for testing.
@@ -63,8 +53,6 @@
                  SystemState* system_state,
                  HttpFetcher* http_fetcher);
   virtual ~DownloadAction();
-  typedef ActionTraits<DownloadAction>::InputObjectType InputObjectType;
-  typedef ActionTraits<DownloadAction>::OutputObjectType OutputObjectType;
   void PerformAction();
   void TerminateProcessing();
 
diff --git a/filesystem_copier_action.h b/filesystem_copier_action.h
index acbdd05..4a61298 100644
--- a/filesystem_copier_action.h
+++ b/filesystem_copier_action.h
@@ -24,25 +24,11 @@
 
 namespace chromeos_update_engine {
 
-class FilesystemCopierAction;
 
-template<>
-class ActionTraits<FilesystemCopierAction> {
- public:
-  // Takes the install plan as input
-  typedef InstallPlan InputObjectType;
-  // Passes the install plan as output
-  typedef InstallPlan OutputObjectType;
-};
-
-class FilesystemCopierAction : public Action<FilesystemCopierAction> {
+class FilesystemCopierAction : public InstallPlanAction {
  public:
   FilesystemCopierAction(bool copying_kernel_install_path, bool verify_hash);
 
-  typedef ActionTraits<FilesystemCopierAction>::InputObjectType
-  InputObjectType;
-  typedef ActionTraits<FilesystemCopierAction>::OutputObjectType
-  OutputObjectType;
   void PerformAction();
   void TerminateProcessing();
 
diff --git a/install_plan.h b/install_plan.h
index d6ff9e0..b070cbe 100644
--- a/install_plan.h
+++ b/install_plan.h
@@ -10,6 +10,8 @@
 
 #include <base/basictypes.h>
 
+#include "update_engine/action.h"
+
 // InstallPlan is a simple struct that contains relevant info for many
 // parts of the update system about the install that should happen.
 namespace chromeos_update_engine {
@@ -39,7 +41,7 @@
   std::string download_url;  // url to download from
 
   uint64_t payload_size;                 // size of the payload
-  std::string payload_hash ;             // SHA256 hash of the payload
+  std::string payload_hash;             // SHA256 hash of the payload
   uint64_t metadata_size;                // size of the metadata
   std::string metadata_signature;        // signature of the  metadata
   std::string install_path;              // path to install device
@@ -70,6 +72,44 @@
   bool powerwash_required;
 };
 
+class InstallPlanAction;
+
+template<>
+class ActionTraits<InstallPlanAction> {
+ public:
+  // Takes the install plan as input
+  typedef InstallPlan InputObjectType;
+  // Passes the install plan as output
+  typedef InstallPlan OutputObjectType;
+};
+
+// Basic action that only receives and sends Install Plans.
+// Can be used to construct an Install Plan to send to any other Action that
+// accept an InstallPlan.
+class InstallPlanAction : public Action<InstallPlanAction> {
+ public:
+  InstallPlanAction() {}
+  InstallPlanAction(const InstallPlan& install_plan):
+    install_plan_(install_plan) {}
+
+  virtual void PerformAction() {
+    if (HasOutputPipe()) {
+      SetOutputObject(install_plan_);
+    }
+    processor_->ActionComplete(this, kErrorCodeSuccess);
+  }
+
+  virtual std::string Type() const { return "InstallPlanAction"; }
+
+  typedef ActionTraits<InstallPlanAction>::InputObjectType InputObjectType;
+  typedef ActionTraits<InstallPlanAction>::OutputObjectType OutputObjectType;
+
+ private:
+  InstallPlan install_plan_;
+
+  DISALLOW_COPY_AND_ASSIGN (InstallPlanAction);
+};
+
 }  // namespace chromeos_update_engine
 
 #endif  // CHROMEOS_PLATFORM_UPDATE_ENGINE_INSTALL_PLAN_H__
diff --git a/omaha_response_handler_action.cc b/omaha_response_handler_action.cc
index c876d94..358988c 100644
--- a/omaha_response_handler_action.cc
+++ b/omaha_response_handler_action.cc
@@ -72,7 +72,7 @@
   }
   install_plan_.is_full_update = !response.is_delta_payload;
 
-  TEST_AND_RETURN(GetInstallDev(
+  TEST_AND_RETURN(utils::GetInstallDev(
       (!boot_device_.empty() ? boot_device_ : utils::BootDevice()),
       &install_plan_.install_path));
   install_plan_.kernel_install_path =
@@ -104,18 +104,6 @@
   completer.set_code(kErrorCodeSuccess);
 }
 
-bool OmahaResponseHandlerAction::GetInstallDev(const std::string& boot_dev,
-                                               std::string* install_dev) {
-  TEST_AND_RETURN_FALSE(utils::StringHasPrefix(boot_dev, "/dev/"));
-  string ret(boot_dev);
-  string::reverse_iterator it = ret.rbegin();  // last character in string
-  // Right now, we just switch '3' and '5' partition numbers.
-  TEST_AND_RETURN_FALSE((*it == '3') || (*it == '5'));
-  *it = (*it == '3') ? '5' : '3';
-  *install_dev = ret;
-  return true;
-}
-
 bool OmahaResponseHandlerAction::AreHashChecksMandatory(
     const OmahaResponse& response) {
   // All our internal testing uses dev server which doesn't generate metadata
diff --git a/omaha_response_handler_action.h b/omaha_response_handler_action.h
index 27d4cec..82e99bf 100644
--- a/omaha_response_handler_action.h
+++ b/omaha_response_handler_action.h
@@ -59,13 +59,6 @@
  private:
   FRIEND_TEST(UpdateAttempterTest, CreatePendingErrorEventResumedTest);
 
-  // Assumes you want to install on the "other" device, where the other
-  // device is what you get if you swap 1 for 2 or 3 for 4 or vice versa
-  // for the number at the end of the boot device. E.g., /dev/sda1 -> /dev/sda2
-  // or /dev/sda4 -> /dev/sda3
-  static bool GetInstallDev(const std::string& boot_dev,
-                            std::string* install_dev);
-
   // Returns true if payload hash checks are mandatory based on the state
   // of the system and the contents of the Omaha response. False otherwise.
   bool AreHashChecksMandatory(const OmahaResponse& response);
diff --git a/postinstall_runner_action.cc b/postinstall_runner_action.cc
index 20dfae9..1143447 100644
--- a/postinstall_runner_action.cc
+++ b/postinstall_runner_action.cc
@@ -23,8 +23,8 @@
 
 void PostinstallRunnerAction::PerformAction() {
   CHECK(HasInputObject());
-  const InstallPlan install_plan = GetInputObject();
-  const string install_device = install_plan.install_path;
+  install_plan_ = GetInputObject();
+  const string install_device = install_plan_.install_path;
   ScopedActionCompleter completer(processor_, this);
 
   // Make mountpoint.
@@ -38,8 +38,10 @@
                  "ext2",
                  mountflags,
                  NULL);
+  // TODO(sosa): Remove once crbug.com/208022 is resolved.
   if (rc < 0) {
-    LOG(INFO) << "Failed to mount install part as ext2. Trying ext3.";
+    LOG(INFO) << "Failed to mount install part "
+              << install_device << " as ext2. Trying ext3.";
     rc = mount(install_device.c_str(),
                temp_rootfs_dir_.c_str(),
                "ext3",
@@ -55,7 +57,7 @@
   temp_dir_remover.set_should_remove(false);
   completer.set_should_complete(false);
 
-  if (install_plan.powerwash_required) {
+  if (install_plan_.powerwash_required) {
     if (utils::CreatePowerwashMarkerFile()) {
       powerwash_marker_created_ = true;
     } else {
@@ -94,11 +96,9 @@
   }
 
   LOG(INFO) << "Postinst command succeeded";
-  CHECK(HasInputObject());
-  const InstallPlan install_plan = GetInputObject();
-
-  if (HasOutputPipe())
-    SetOutputObject(install_plan);
+  if (HasOutputPipe()) {
+    SetOutputObject(install_plan_);
+  }
 
   completer.set_code(kErrorCodeSuccess);
 }
diff --git a/postinstall_runner_action.h b/postinstall_runner_action.h
index 6979975..2308a0f 100644
--- a/postinstall_runner_action.h
+++ b/postinstall_runner_action.h
@@ -15,27 +15,10 @@
 
 namespace chromeos_update_engine {
 
-class PostinstallRunnerAction;
-class NoneType;
-
-template<>
-class ActionTraits<PostinstallRunnerAction> {
+class PostinstallRunnerAction : public InstallPlanAction {
  public:
-  // Takes the device path as input
-  typedef InstallPlan InputObjectType;
-  // Passes the device path as output
-  typedef InstallPlan OutputObjectType;
-};
+  PostinstallRunnerAction(): powerwash_marker_created_(false) {}
 
-class PostinstallRunnerAction : public Action<PostinstallRunnerAction> {
- public:
-  PostinstallRunnerAction()
-    : powerwash_marker_created_(false) {}
-
-  typedef ActionTraits<PostinstallRunnerAction>::InputObjectType
-      InputObjectType;
-  typedef ActionTraits<PostinstallRunnerAction>::OutputObjectType
-      OutputObjectType;
   void PerformAction();
 
   // Note that there's no support for terminating this action currently.
@@ -52,6 +35,7 @@
                                         const std::string& output,
                                         void* p);
 
+  InstallPlan install_plan_;
   std::string temp_rootfs_dir_;
 
   // True if Powerwash Marker was created before invoking post-install script.
diff --git a/update_attempter.cc b/update_attempter.cc
index 3aa6a1e..3a6988e 100644
--- a/update_attempter.cc
+++ b/update_attempter.cc
@@ -77,6 +77,8 @@
       return "UPDATE_STATUS_UPDATED_NEED_REBOOT";
     case UPDATE_STATUS_REPORTING_ERROR_EVENT:
       return "UPDATE_STATUS_REPORTING_ERROR_EVENT";
+    case UPDATE_STATUS_ATTEMPTING_ROLLBACK:
+      return "UPDATE_STATUS_ATTEMPTING_ROLLBACK";
     default:
       return "unknown status";
   }
@@ -433,6 +435,15 @@
                    omaha_request_params_->waiting_period().InSeconds());
 }
 
+void UpdateAttempter::BuildPostInstallActions(
+    InstallPlanAction* previous_action) {
+  shared_ptr<PostinstallRunnerAction> postinstall_runner_action(
+        new PostinstallRunnerAction());
+  actions_.push_back(shared_ptr<AbstractAction>(postinstall_runner_action));
+  BondActions(previous_action,
+              postinstall_runner_action.get());
+}
+
 void UpdateAttempter::BuildUpdateActions(bool interactive) {
   CHECK(!processor_->IsRunning());
   processor_->set_delegate(this);
@@ -483,8 +494,6 @@
       new FilesystemCopierAction(false, true));
   shared_ptr<FilesystemCopierAction> kernel_filesystem_verifier_action(
       new FilesystemCopierAction(true, true));
-  shared_ptr<PostinstallRunnerAction> postinstall_runner_action(
-      new PostinstallRunnerAction);
   shared_ptr<OmahaRequestAction> update_complete_action(
       new OmahaRequestAction(system_state_,
                              new OmahaEvent(OmahaEvent::kTypeUpdateComplete),
@@ -506,16 +515,8 @@
   actions_.push_back(shared_ptr<AbstractAction>(download_action));
   actions_.push_back(shared_ptr<AbstractAction>(download_finished_action));
   actions_.push_back(shared_ptr<AbstractAction>(filesystem_verifier_action));
-  actions_.push_back(shared_ptr<AbstractAction>(
-      kernel_filesystem_verifier_action));
-  actions_.push_back(shared_ptr<AbstractAction>(postinstall_runner_action));
-  actions_.push_back(shared_ptr<AbstractAction>(update_complete_action));
-
-  // Enqueue the actions
-  for (vector<shared_ptr<AbstractAction> >::iterator it = actions_.begin();
-       it != actions_.end(); ++it) {
-    processor_->EnqueueAction(it->get());
-  }
+    actions_.push_back(shared_ptr<AbstractAction>(
+        kernel_filesystem_verifier_action));
 
   // Bond them together. We have to use the leaf-types when calling
   // BondActions().
@@ -531,8 +532,52 @@
               filesystem_verifier_action.get());
   BondActions(filesystem_verifier_action.get(),
               kernel_filesystem_verifier_action.get());
-  BondActions(kernel_filesystem_verifier_action.get(),
-              postinstall_runner_action.get());
+
+  BuildPostInstallActions(kernel_filesystem_verifier_action.get());
+
+  actions_.push_back(shared_ptr<AbstractAction>(update_complete_action));
+
+  // Enqueue the actions
+  for (vector<shared_ptr<AbstractAction> >::iterator it = actions_.begin();
+       it != actions_.end(); ++it) {
+    processor_->EnqueueAction(it->get());
+  }
+}
+
+void UpdateAttempter::Rollback(bool powerwash) {
+  CHECK(!processor_->IsRunning());
+  processor_->set_delegate(this);
+
+  LOG(INFO) << "Setting rollback options.";
+  InstallPlan install_plan;
+  TEST_AND_RETURN(utils::GetInstallDev(utils::BootDevice(),
+                                       &install_plan.install_path));
+  install_plan.kernel_install_path = utils::BootKernelDevice(
+      install_plan.install_path);
+  install_plan.powerwash_required = powerwash;
+
+  LOG(INFO) << "Using this install plan:";
+  install_plan.Dump();
+
+  shared_ptr<InstallPlanAction> install_plan_action(
+      new InstallPlanAction(install_plan));
+  actions_.push_back(shared_ptr<AbstractAction>(install_plan_action));
+
+  BuildPostInstallActions(install_plan_action.get());
+
+  // Enqueue the actions
+  for (vector<shared_ptr<AbstractAction> >::iterator it = actions_.begin();
+       it != actions_.end(); ++it) {
+    processor_->EnqueueAction(it->get());
+  }
+  SetStatusAndNotify(UPDATE_STATUS_ATTEMPTING_ROLLBACK,
+                     kUpdateNoticeUnspecified);
+
+  // Just in case we didn't update boot flags yet, make sure they're updated
+  // before any update processing starts. This also schedules the start of the
+  // actions we just posted.
+  start_action_processor_ = true;
+  UpdateBootFlags();
 }
 
 void UpdateAttempter::CheckForUpdate(const string& app_version,
@@ -574,7 +619,6 @@
 // Delegate methods:
 void UpdateAttempter::ProcessingDone(const ActionProcessor* processor,
                                      ErrorCode code) {
-  CHECK(response_handler_action_);
   LOG(INFO) << "Processing Done.";
   actions_.clear();
 
@@ -1132,4 +1176,5 @@
   prefs_->Delete(kPrefsUpdateCheckCount);
   return false;
 }
+
 }  // namespace chromeos_update_engine
diff --git a/update_attempter.h b/update_attempter.h
index 315e545..d33d69e 100644
--- a/update_attempter.h
+++ b/update_attempter.h
@@ -18,6 +18,7 @@
 #include "update_engine/action_processor.h"
 #include "update_engine/chrome_browser_proxy_resolver.h"
 #include "update_engine/download_action.h"
+#include "update_engine/filesystem_copier_action.h"
 #include "update_engine/omaha_request_params.h"
 #include "update_engine/omaha_response_handler_action.h"
 #include "update_engine/proxy_resolver.h"
@@ -45,6 +46,7 @@
   UPDATE_STATUS_FINALIZING,
   UPDATE_STATUS_UPDATED_NEED_REBOOT,
   UPDATE_STATUS_REPORTING_ERROR_EVENT,
+  UPDATE_STATUS_ATTEMPTING_ROLLBACK
 };
 
 enum UpdateNotice {
@@ -142,6 +144,12 @@
                       const std::string& omaha_url,
                       bool is_interactive);
 
+  // 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
+  // the partition to boot from. If |powerwash| is True, perform a powerwash
+  // as part of rollback.
+  void Rollback(bool powerwash);
+
   // Initiates a reboot if the current state is
   // UPDATED_NEED_REBOOT. Returns true on sucess, false otherwise.
   bool RebootIfNeeded();
@@ -270,9 +278,15 @@
   // scatter factor value specified from policy.
   void GenerateNewWaitingPeriod();
 
+  // Helper method of Update() and Rollback() to construct the sequence of
+  // actions to be performed for the postinstall.
+  // |previous_action| is the previous action to get
+  // bonded with the install_plan that gets passed to postinstall.
+  void BuildPostInstallActions(InstallPlanAction* previous_action);
+
   // Helper method of Update() to construct the sequence of actions to
   // be performed for an update check. Please refer to
-  // Update() method for the meaning of the parametes.
+  // Update() method for the meaning of the parameters.
   void BuildUpdateActions(bool interactive);
 
   // Decrements the count in the kUpdateCheckCountFilePath.
diff --git a/update_engine.xml b/update_engine.xml
index 0ce0962..195bde8 100644
--- a/update_engine.xml
+++ b/update_engine.xml
@@ -11,6 +11,9 @@
       <arg type="s" name="app_version" />
       <arg type="s" name="omaha_url" />
     </method>
+    <method name="AttemptRollback">
+      <arg type="b" name="powerwash" />
+    </method>
     <method name="ResetStatus">
     </method>
     <method name="GetStatus">
diff --git a/update_engine_client.cc b/update_engine_client.cc
index e28801c..b4a4bda 100644
--- a/update_engine_client.cc
+++ b/update_engine_client.cc
@@ -23,15 +23,17 @@
 using std::string;
 
 DEFINE_string(app_version, "", "Force the current app version.");
-DEFINE_bool(check_for_update, false, "Initiate check for updates.");
-DEFINE_string(omaha_url, "", "The URL of the Omaha update server.");
-DEFINE_bool(reboot, false, "Initiate a reboot if needed.");
-DEFINE_bool(show_channel, false, "Show the current and target channels.");
-DEFINE_bool(status, false, "Print the status to stdout.");
-DEFINE_bool(reset_status, false, "Sets the status in update_engine to idle.");
 DEFINE_string(channel, "",
     "Set the target channel. The device will be powerwashed if the target "
     "channel is more stable than the current channel.");
+DEFINE_bool(check_for_update, false, "Initiate check for updates.");
+DEFINE_string(omaha_url, "", "The URL of the Omaha update server.");
+DEFINE_bool(reboot, false, "Initiate a reboot if needed.");
+DEFINE_bool(reset_status, false, "Sets the status in update_engine to idle.");
+DEFINE_bool(rollback, false, "Perform a rollback to the previous partition.");
+DEFINE_bool(show_channel, false, "Show the current and target channels.");
+DEFINE_bool(powerwash, true, "When performing a rollback, do a powerwash.");
+DEFINE_bool(status, false, "Print the status to stdout.");
 DEFINE_bool(update, false, "Forces an update and waits for its completion. "
             "Exit status is 0 if the update succeeded, and 1 otherwise.");
 DEFINE_bool(watch_for_updates, false,
@@ -176,6 +178,21 @@
   g_main_loop_unref(loop);
 }
 
+bool Rollback(bool rollback) {
+  DBusGProxy* proxy;
+  GError* error = NULL;
+
+  CHECK(GetProxy(&proxy));
+
+  gboolean rc =
+      org_chromium_UpdateEngineInterface_attempt_rollback(proxy,
+                                                          rollback,
+                                                          &error);
+  CHECK_EQ(rc, TRUE) << "Error with rollback request: "
+                     << GetAndFreeGError(&error);
+  return true;
+}
+
 bool CheckForUpdates(const string& app_version, const string& omaha_url) {
   DBusGProxy* proxy;
   GError* error = NULL;
@@ -313,11 +330,27 @@
       LOG(INFO) << "Target Channel (pending update): " << target_channel;
   }
 
+  bool do_update_request = FLAGS_check_for_update | FLAGS_update |
+      !FLAGS_app_version.empty() | !FLAGS_omaha_url.empty();
+
+  if (!FLAGS_powerwash && !FLAGS_rollback) {
+    LOG(FATAL) << "Skipping powerwash only works with rollback";
+    return 1;
+  }
+
+  if (do_update_request && FLAGS_rollback) {
+    LOG(FATAL) << "Update should not be requested with rollback!";
+    return 1;
+  }
+
+  if(FLAGS_rollback) {
+    LOG(INFO) << "Requesting rollback.";
+    CHECK(Rollback(FLAGS_powerwash)) << "Request for rollback failed.";
+    return 0;
+  }
+
   // Initiate an update check, if necessary.
-  if (FLAGS_check_for_update ||
-      FLAGS_update ||
-      !FLAGS_app_version.empty() ||
-      !FLAGS_omaha_url.empty()) {
+  if (do_update_request) {
     LOG_IF(WARNING, FLAGS_reboot) << "-reboot flag ignored.";
     string app_version = FLAGS_app_version;
     if (FLAGS_update && app_version.empty()) {
diff --git a/utils.cc b/utils.cc
index d24a61f..62581f4 100644
--- a/utils.cc
+++ b/utils.cc
@@ -1021,6 +1021,17 @@
   return result;
 }
 
+bool GetInstallDev(const std::string& boot_dev, std::string* install_dev) {
+  TEST_AND_RETURN_FALSE(StringHasPrefix(boot_dev, "/dev/"));
+  string ret(boot_dev);
+  string::reverse_iterator it = ret.rbegin();  // last character in string
+  // Right now, we just switch '3' and '5' partition numbers.
+  TEST_AND_RETURN_FALSE((*it == '3') || (*it == '5'));
+  *it = (*it == '3') ? '5' : '3';
+  *install_dev = ret;
+  return true;
+}
+
 }  // namespace utils
 
 }  // namespace chromeos_update_engine
diff --git a/utils.h b/utils.h
index 5193845..0fe2948 100644
--- a/utils.h
+++ b/utils.h
@@ -323,6 +323,13 @@
 // Returns true if successfully deleted. False otherwise.
 bool DeletePowerwashMarkerFile();
 
+// Assumes you want to install on the "other" device, where the other
+// device is what you get if you swap 1 for 2 or 3 for 4 or vice versa
+// for the number at the end of the boot device. E.g., /dev/sda1 -> /dev/sda2
+// or /dev/sda4 -> /dev/sda3. See
+// http://www.chromium.org/chromium-os/chromiumos-design-docs/disk-format
+bool GetInstallDev(const std::string& boot_dev, std::string* install_dev);
+
 }  // namespace utils
 
 
diff --git a/utils_unittest.cc b/utils_unittest.cc
index 4d9dea4..c0e0dfa 100644
--- a/utils_unittest.cc
+++ b/utils_unittest.cc
@@ -283,6 +283,20 @@
   EXPECT_EQ(10 * 1024 * 1024 / 4096, block_count);
 }
 
+TEST(UtilsTest, GetInstallDevTest) {
+  string boot_dev = "/dev/sda5";
+  string install_dev;
+  EXPECT_TRUE(utils::GetInstallDev(boot_dev, &install_dev));
+  EXPECT_EQ(install_dev, "/dev/sda3");
+
+  boot_dev = "/dev/sda3";
+  EXPECT_TRUE(utils::GetInstallDev(boot_dev, &install_dev));
+  EXPECT_EQ(install_dev, "/dev/sda5");
+
+  boot_dev = "/dev/sda12";
+  EXPECT_FALSE(utils::GetInstallDev(boot_dev, &install_dev));
+}
+
 namespace {
 gboolean  TerminateScheduleCrashReporterUploadTest(void* arg) {
   GMainLoop* loop = reinterpret_cast<GMainLoop*>(arg);