update_engine: added CanRollback dbus method

Added a method to the update engine to check if a rollback
partition is available and can be booted from.
update_engine_client is also updated to call the function
when --can_rollback paramater is specified

BUG=chromium:343301
TEST=Ran "update_engine_client --can_rollback" and unit tests

Change-Id: If3fcb29a0067069a22812f60e9b67c6fdbbd18bd
Reviewed-on: https://chromium-review.googlesource.com/187157
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Reviewed-by: Don Garrett <dgarrett@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/UpdateEngine.conf b/UpdateEngine.conf
index 8c04eae..ac40e9d 100644
--- a/UpdateEngine.conf
+++ b/UpdateEngine.conf
@@ -17,6 +17,9 @@
            send_member="AttemptRollback"/>
     <allow send_destination="org.chromium.UpdateEngine"
            send_interface="org.chromium.UpdateEngineInterface"
+           send_member="CanRollback"/>
+    <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 f75823c..5562f4e 100644
--- a/dbus_service.cc
+++ b/dbus_service.cc
@@ -192,6 +192,16 @@
   return TRUE;
 }
 
+gboolean update_engine_service_can_rollback(UpdateEngineService* self,
+                                            gboolean* out_can_rollback,
+                                            GError **error)
+{
+  LOG(INFO) << "Checking for a rollback partition.";
+  *out_can_rollback = self->system_state_->update_attempter()->CanRollback();
+  return TRUE;
+}
+
+
 gboolean update_engine_service_reset_status(UpdateEngineService* self,
                                             GError **error) {
   if (!self->system_state_->update_attempter()->ResetStatus()) {
diff --git a/dbus_service.h b/dbus_service.h
index 5d0f500..32bb599 100644
--- a/dbus_service.h
+++ b/dbus_service.h
@@ -62,6 +62,13 @@
                                                 gboolean powerwash,
                                                 GError **error);
 
+// Checks if the system rollback is available by verifying if the secondary
+// system partition is valid and bootable.
+gboolean update_engine_service_can_rollback(
+    UpdateEngineService* self,
+    gboolean* out_can_rollback,
+    GError **error);
+
 gboolean update_engine_service_reset_status(UpdateEngineService* self,
                                             GError **error);
 
diff --git a/fake_hardware.h b/fake_hardware.h
index 97ab8dd..6f4be77 100644
--- a/fake_hardware.h
+++ b/fake_hardware.h
@@ -17,6 +17,7 @@
   FakeHardware()
     : kernel_device_("/dev/sdz4"),
       boot_device_("/dev/sdz5"),
+      bootable_devices_{"/dev/sdz4", "/dev/sdz5"},
       is_official_build_(true),
       is_normal_boot_mode_(true),
       hardware_class_("Fake HWID BLAH-1234"),
@@ -26,6 +27,9 @@
   // HardwareInterface methods.
   virtual const std::string BootKernelDevice() { return kernel_device_; }
   virtual const std::string BootDevice() { return boot_device_; }
+  virtual std::vector<std::string> GetKernelDevices() override
+      { return bootable_devices_; }
+
   virtual bool IsKernelBootable(const std::string& kernel_device,
                                 bool* bootable)
       { std::map<std::string, bool>::const_iterator i =
@@ -70,6 +74,7 @@
  private:
   std::string kernel_device_;
   std::string boot_device_;
+  std::vector<std::string>  bootable_devices_;
   std::map<std::string, bool> is_bootable_;
   bool is_official_build_;
   bool is_normal_boot_mode_;
diff --git a/hardware.cc b/hardware.cc
index 9f3fe00..d0e0fc0 100644
--- a/hardware.cc
+++ b/hardware.cc
@@ -76,6 +76,33 @@
   return true;
 }
 
+std::vector<std::string> Hardware::GetKernelDevices() {
+  LOG(INFO) << "GetAllKernelDevices";
+
+  std::string disk_name = utils::GetDiskName(Hardware::BootKernelDevice());
+  if(disk_name.empty()) {
+    LOG(ERROR) << "Failed to get the cuurent kernel boot disk name";
+    return std::vector<std::string>();
+  }
+
+  std::vector<std::string> devices;
+  const int slot_count = 2; // Use only partition slots A and B
+  devices.reserve(slot_count);
+  for(int slot = 0; slot < slot_count; slot++) {
+    int partition_num = (slot + 1) * 2; // for now, only #2, #4
+    std::string device = utils::MakePartitionName(disk_name, partition_num);
+    if(!device.empty()) {
+      devices.push_back(std::move(device));
+    } else {
+      LOG(ERROR) << "Cannot make a partition name for disk: "
+                 << disk_name << ", partition: " << partition_num;
+    }
+  }
+
+  return devices;
+}
+
+
 bool Hardware::MarkKernelUnbootable(const std::string& kernel_device) {
   LOG(INFO) << "MarkPartitionUnbootable: " << kernel_device;
 
diff --git a/hardware.h b/hardware.h
index 8168d9c..cd7c8d5 100644
--- a/hardware.h
+++ b/hardware.h
@@ -20,6 +20,7 @@
   // HardwareInterface methods.
   virtual const std::string BootKernelDevice();
   virtual const std::string BootDevice();
+  virtual std::vector<std::string> GetKernelDevices() override;
   virtual bool IsKernelBootable(const std::string& kernel_device,
                                 bool* bootable);
   virtual bool MarkKernelUnbootable(const std::string& kernel_device);
diff --git a/hardware_interface.h b/hardware_interface.h
index 29baec4..58157d0 100644
--- a/hardware_interface.h
+++ b/hardware_interface.h
@@ -6,6 +6,7 @@
 #define CHROMEOS_PLATFORM_UPDATE_ENGINE_HARDWARE_INTERFACE_H__
 
 #include <string>
+#include <vector>
 
 namespace chromeos_update_engine {
 
@@ -23,6 +24,9 @@
   // Returns the currently booted rootfs partition. "/dev/sda3", for example.
   virtual const std::string BootDevice() = 0;
 
+  // Returns a list of all kernel partitions available (whether bootable or not)
+  virtual std::vector<std::string> GetKernelDevices() = 0;
+
   // Is the specified kernel partition currently bootable, based on GPT flags?
   // Returns success.
   virtual bool IsKernelBootable(const std::string& kernel_device,
diff --git a/mock_hardware.h b/mock_hardware.h
index 6a27488..8257b91 100644
--- a/mock_hardware.h
+++ b/mock_hardware.h
@@ -22,6 +22,9 @@
     ON_CALL(*this, BootDevice())
       .WillByDefault(testing::Invoke(&fake_,
             &FakeHardware::BootDevice));
+    ON_CALL(*this, GetKernelDevices())
+      .WillByDefault(testing::Invoke(&fake_,
+            &FakeHardware::GetKernelDevices));
     ON_CALL(*this, IsKernelBootable(testing::_, testing::_))
       .WillByDefault(testing::Invoke(&fake_,
             &FakeHardware::IsKernelBootable));
@@ -50,6 +53,7 @@
   // Hardware overrides.
   MOCK_METHOD0(BootKernelDevice, const std::string());
   MOCK_METHOD0(BootDevice, const std::string());
+  MOCK_METHOD0(GetKernelDevices, std::vector<std::string>());
   MOCK_METHOD2(IsKernelBootable,
                bool(const std::string& kernel_device, bool* bootable));
   MOCK_METHOD1(MarkKernelUnbootable,
diff --git a/update_attempter.cc b/update_attempter.cc
index 7b34cee..dcde2cf 100644
--- a/update_attempter.cc
+++ b/update_attempter.cc
@@ -7,6 +7,7 @@
 #include <string>
 #include <tr1/memory>
 #include <vector>
+#include <algorithm>
 
 #include <base/file_util.h>
 #include <base/logging.h>
@@ -788,6 +789,35 @@
   return true;
 }
 
+bool UpdateAttempter::CanRollback() const {
+  std::vector<std::string> kernel_devices =
+      system_state_->hardware()->GetKernelDevices();
+
+  std::string boot_kernel_device =
+      system_state_->hardware()->BootKernelDevice();
+
+  auto current = std::find(kernel_devices.begin(), kernel_devices.end(),
+                           boot_kernel_device);
+
+  if(current == kernel_devices.end()) {
+    LOG(ERROR) << "Unable to find the boot kernel device in the list of "
+               << "available devices";
+    return false;
+  }
+
+  for (std::string const& device_name : kernel_devices) {
+    if (device_name != *current) {
+      bool bootable = false;
+      if (system_state_->hardware()->IsKernelBootable(device_name, &bootable) &&
+          bootable) {
+        return true;
+      }
+    }
+  }
+
+  return false;
+}
+
 void UpdateAttempter::CheckForUpdate(const string& app_version,
                                      const string& omaha_url,
                                      bool interactive) {
diff --git a/update_attempter.h b/update_attempter.h
index 70381d9..d5c6adb 100644
--- a/update_attempter.h
+++ b/update_attempter.h
@@ -149,6 +149,10 @@
   // the partitions to roll back to (used in testing). Returns True on success.
   bool Rollback(bool powerwash, std::string* install_path);
 
+  // This is the internal entry point for checking if a valid rollback
+  // partition exists.
+  bool CanRollback() const;
+
   // Initiates a reboot if the current state is
   // UPDATED_NEED_REBOOT. Returns true on sucess, false otherwise.
   bool RebootIfNeeded();
diff --git a/update_engine.xml b/update_engine.xml
index 5a2b722..964ea62 100644
--- a/update_engine.xml
+++ b/update_engine.xml
@@ -26,6 +26,9 @@
     <method name="AttemptRollback">
       <arg type="b" name="powerwash" />
     </method>
+    <method name="CanRollback">
+      <arg type="b" name="can_rollback" direction="out" />
+    </method>
     <method name="ResetStatus">
     </method>
     <method name="GetStatus">
diff --git a/update_engine_client.cc b/update_engine_client.cc
index fc06da2..6bd3d9f 100644
--- a/update_engine_client.cc
+++ b/update_engine_client.cc
@@ -42,6 +42,8 @@
 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(can_rollback, false, "Shows whether rollback partition "
+            "is available.");
 DEFINE_bool(show_channel, false, "Show the current and target channels.");
 DEFINE_bool(show_p2p_update, false,
             "Show the current setting for peer-to-peer update sharing.");
@@ -207,6 +209,20 @@
   return true;
 }
 
+bool CanRollback() {
+  DBusGProxy* proxy;
+  GError* error = NULL;
+
+  CHECK(GetProxy(&proxy));
+
+  gboolean can_rollback = FALSE;
+  gboolean rc = update_engine_client_can_rollback(proxy,
+                                                  &can_rollback,
+                                                  &error);
+  CHECK_EQ(rc, TRUE) << "Error while querying rollback partition availabilty: "
+                     << GetAndFreeGError(&error);
+  return can_rollback;
+}
 bool CheckForUpdates(const string& app_version, const string& omaha_url) {
   DBusGProxy* proxy;
   GError* error = NULL;
@@ -412,6 +428,13 @@
     }
   }
 
+  // Show the rollback availability.
+  if (FLAGS_can_rollback) {
+    bool can_rollback = CanRollback();
+    LOG(INFO) << "Rollback partition: "
+              << (can_rollback ? "AVAILABLE" : "UNAVAILABLE");
+  }
+
   // Show the current P2P enabled setting.
   if (FLAGS_show_p2p_update) {
     bool enabled = GetP2PUpdatePermission();