update_engine: Support milestones to EOL from Omaha

Initiative to show EOL message on Chrome OS devices require that
update_engine parses the fields within Omaha response that pertain to the
new milestones to EOL field. The Omaha response will include a new
field called "milestones_to_eol" which will be an integer value
string.

The job of update_engine when it comes to milestones to EOL from Omaha
is to merely forward. No checks and no modifications of fields are
done within update_engine besides being able to convert the milestones
to EOL from a string to integer.

BUG=chromium:994999
TEST=FEATURES="test" emerge-$BOARD update_engine update_engine-client
TEST=cros deploy $IP update_engine update_engine-client
TEST=test_that -b $BOARD $IP autoupdate_EOL # from Cq-Depend
TEST=test_that -b $BOARD $IP autoupdate_EOL.milestones # from Cq-Depend

Cq-Depend:chromium:1761371
Change-Id: I268e4c8e641b17d6a727a50f53285cc97c76eb22
Reviewed-on: https://chromium-review.googlesource.com/1759285
Tested-by: Jae Hoon Kim <kimjae@chromium.org>
Commit-Ready: ChromeOS CL Exonerator Bot <chromiumos-cl-exonerator@appspot.gserviceaccount.com>
Legacy-Commit-Queue: Commit Bot <commit-bot@chromium.org>
Reviewed-by: Nicolas Norvez <norvez@chromium.org>
Reviewed-by: Amin Hassani <ahassani@chromium.org>
diff --git a/client_library/client_dbus.cc b/client_library/client_dbus.cc
index e2defe7..d0f7f81 100644
--- a/client_library/client_dbus.cc
+++ b/client_library/client_dbus.cc
@@ -270,8 +270,9 @@
   return proxy_->GetLastAttemptError(last_attempt_error, nullptr);
 }
 
-bool DBusUpdateEngineClient::GetEolStatus(int32_t* eol_status) const {
-  return proxy_->GetEolStatus(eol_status, nullptr);
+bool DBusUpdateEngineClient::GetEolStatus(int32_t* eol_status,
+                                          int32_t* milestones_to_eol) const {
+  return proxy_->GetEolStatus(eol_status, milestones_to_eol, nullptr);
 }
 
 }  // namespace internal
diff --git a/client_library/client_dbus.h b/client_library/client_dbus.h
index c9631cf..3f782e9 100644
--- a/client_library/client_dbus.h
+++ b/client_library/client_dbus.h
@@ -85,7 +85,8 @@
 
   bool GetLastAttemptError(int32_t* last_attempt_error) const override;
 
-  bool GetEolStatus(int32_t* eol_status) const override;
+  bool GetEolStatus(int32_t* eol_status,
+                    int32_t* milestones_to_eol) const override;
 
  private:
   void DBusStatusHandlersRegistered(const std::string& interface,
diff --git a/client_library/include/update_engine/client.h b/client_library/include/update_engine/client.h
index 89f36af..65a3267 100644
--- a/client_library/include/update_engine/client.h
+++ b/client_library/include/update_engine/client.h
@@ -135,8 +135,10 @@
   // Get the last UpdateAttempt error code.
   virtual bool GetLastAttemptError(int32_t* last_attempt_error) const = 0;
 
-  // Get the current end-of-life status code. See EolStatus enum for details.
-  virtual bool GetEolStatus(int32_t* eol_status) const = 0;
+  // Get the current end-of-life status code and milestones to end-of-life.
+  // See |EolStatus| enum and |MilestonesToEol| enum for details.
+  virtual bool GetEolStatus(int32_t* eol_status,
+                            int32_t* milestones_to_eol) const = 0;
 
  protected:
   // Use CreateInstance().
diff --git a/common/constants.cc b/common/constants.cc
index 87bdf91..6f37e16 100644
--- a/common/constants.cc
+++ b/common/constants.cc
@@ -56,6 +56,7 @@
 const char kPrefsOmahaCohortHint[] = "omaha-cohort-hint";
 const char kPrefsOmahaCohortName[] = "omaha-cohort-name";
 const char kPrefsOmahaEolStatus[] = "omaha-eol-status";
+const char kPrefsOmahaMilestonesToEol[] = "omaha-milestones-to-eol";
 const char kPrefsP2PEnabled[] = "p2p-enabled";
 const char kPrefsP2PFirstAttemptTimestamp[] = "p2p-first-attempt-timestamp";
 const char kPrefsP2PNumAttempts[] = "p2p-num-attempts";
diff --git a/common/constants.h b/common/constants.h
index d95a56a..6034dbd 100644
--- a/common/constants.h
+++ b/common/constants.h
@@ -57,6 +57,7 @@
 extern const char kPrefsOmahaCohortHint[];
 extern const char kPrefsOmahaCohortName[];
 extern const char kPrefsOmahaEolStatus[];
+extern const char kPrefsOmahaMilestonesToEol[];
 extern const char kPrefsP2PEnabled[];
 extern const char kPrefsP2PFirstAttemptTimestamp[];
 extern const char kPrefsP2PNumAttempts[];
diff --git a/common_service.cc b/common_service.cc
index 0d5ee6d..466007f 100644
--- a/common_service.cc
+++ b/common_service.cc
@@ -413,18 +413,31 @@
 }
 
 bool UpdateEngineService::GetEolStatus(ErrorPtr* error,
-                                       int32_t* out_eol_status) {
+                                       int32_t* out_eol_status,
+                                       int32_t* out_milestones_to_eol) {
   PrefsInterface* prefs = system_state_->prefs();
 
+  // Set EOL.
   string str_eol_status;
   if (prefs->Exists(kPrefsOmahaEolStatus) &&
       !prefs->GetString(kPrefsOmahaEolStatus, &str_eol_status)) {
     LogAndSetError(error, FROM_HERE, "Error getting the end-of-life status.");
     return false;
   }
-
-  // StringToEolStatus will return kSupported for invalid values.
+  // |StringToEolStatus()| will return |kSupported| for invalid values.
   *out_eol_status = static_cast<int32_t>(StringToEolStatus(str_eol_status));
+
+  // Set milestones to EOL.
+  string str_milestones_to_eol;
+  if (prefs->Exists(kPrefsOmahaMilestonesToEol) &&
+      !prefs->GetString(kPrefsOmahaMilestonesToEol, &str_milestones_to_eol)) {
+    LogAndSetError(error, FROM_HERE, "Error getting the milestones to EOL.");
+    return false;
+  }
+  // |StringToMilestonesToEol()| will return |kMilestonesToEolNone| for invalid
+  // values.
+  *out_milestones_to_eol = StringToMilestonesToEol(str_milestones_to_eol);
+
   return true;
 }
 
diff --git a/common_service.h b/common_service.h
index f93855d..5244e99 100644
--- a/common_service.h
+++ b/common_service.h
@@ -153,9 +153,12 @@
   bool GetLastAttemptError(brillo::ErrorPtr* error,
                            int32_t* out_last_attempt_error);
 
-  // Returns the current end-of-life status of the device. This value is updated
-  // on every update check and persisted on disk across reboots.
-  bool GetEolStatus(brillo::ErrorPtr* error, int32_t* out_eol_status);
+  // Returns the current EOL status of the device and the milestones to
+  // EOL if marked EOL. The values are updated on every update check and
+  // persisted on disk across reboots.
+  bool GetEolStatus(brillo::ErrorPtr* error,
+                    int32_t* out_eol_status,
+                    int32_t* out_milestones_to_eol);
 
  private:
   SystemState* system_state_;
diff --git a/common_service_unittest.cc b/common_service_unittest.cc
index 65202a0..68b2468 100644
--- a/common_service_unittest.cc
+++ b/common_service_unittest.cc
@@ -169,19 +169,90 @@
                                UpdateEngineService::kErrorFailed));
 }
 
-TEST_F(UpdateEngineServiceTest, GetEolStatusTest) {
+TEST_F(UpdateEngineServiceTest, GetEolStatusTestWithMilestonesDefault) {
   FakePrefs fake_prefs;
   fake_system_state_.set_prefs(&fake_prefs);
-  // The default value should be "supported".
+  // The default value for EOL be |kSupported| and milestone
+  // |kMilestonesToEolNone|.
   int32_t eol_status = static_cast<int32_t>(EolStatus::kEol);
-  EXPECT_TRUE(common_service_.GetEolStatus(&error_, &eol_status));
+  MilestonesToEol milestones_to_eol = kMilestonesToEolNone;
+  EXPECT_TRUE(
+      common_service_.GetEolStatus(&error_, &eol_status, &milestones_to_eol));
   EXPECT_EQ(nullptr, error_);
   EXPECT_EQ(EolStatus::kSupported, static_cast<EolStatus>(eol_status));
+  EXPECT_EQ(kMilestonesToEolNone, milestones_to_eol);
+}
 
-  fake_prefs.SetString(kPrefsOmahaEolStatus, "security-only");
-  EXPECT_TRUE(common_service_.GetEolStatus(&error_, &eol_status));
+TEST_F(UpdateEngineServiceTest, GetEolStatusMilestonesToEolNone) {
+  FakePrefs fake_prefs;
+  fake_system_state_.set_prefs(&fake_prefs);
+  int32_t eol_status = static_cast<int32_t>(EolStatus::kEol);
+  MilestonesToEol milestones_to_eol = kMilestonesToEolNone;
+
+  // Device is supported and no milestones to EOL set.
+  fake_prefs.SetString(kPrefsOmahaEolStatus, kEolStatusSupported);
+  EXPECT_TRUE(
+      common_service_.GetEolStatus(&error_, &eol_status, &milestones_to_eol));
+  EXPECT_EQ(nullptr, error_);
+  EXPECT_EQ(EolStatus::kSupported, static_cast<EolStatus>(eol_status));
+  EXPECT_EQ(kMilestonesToEolNone, milestones_to_eol);
+
+  // Device is security only and no milestones to EOL set.
+  fake_prefs.SetString(kPrefsOmahaEolStatus, kEolStatusSecurityOnly);
+  EXPECT_TRUE(
+      common_service_.GetEolStatus(&error_, &eol_status, &milestones_to_eol));
   EXPECT_EQ(nullptr, error_);
   EXPECT_EQ(EolStatus::kSecurityOnly, static_cast<EolStatus>(eol_status));
+  EXPECT_EQ(kMilestonesToEolNone, milestones_to_eol);
+
+  // Device is EOL and no milestones to EOL set.
+  fake_prefs.SetString(kPrefsOmahaEolStatus, kEolStatusEol);
+  EXPECT_TRUE(
+      common_service_.GetEolStatus(&error_, &eol_status, &milestones_to_eol));
+  EXPECT_EQ(nullptr, error_);
+  EXPECT_EQ(EolStatus::kEol, static_cast<EolStatus>(eol_status));
+  EXPECT_EQ(kMilestonesToEolNone, milestones_to_eol);
+}
+
+TEST_F(UpdateEngineServiceTest, GetEolStatusMilestonesToEolTwo) {
+  FakePrefs fake_prefs;
+  fake_system_state_.set_prefs(&fake_prefs);
+  int32_t eol_status = static_cast<int32_t>(EolStatus::kEol);
+  MilestonesToEol milestones_to_eol = kMilestonesToEolNone;
+
+  // Device is supported and milestones to EOL is n-2.
+  fake_prefs.SetString(kPrefsOmahaEolStatus, kEolStatusSupported);
+  fake_prefs.SetString(kPrefsOmahaMilestonesToEol, "2");
+  EXPECT_TRUE(
+      common_service_.GetEolStatus(&error_, &eol_status, &milestones_to_eol));
+  EXPECT_EQ(nullptr, error_);
+  EXPECT_EQ(EolStatus::kSupported, static_cast<EolStatus>(eol_status));
+  EXPECT_EQ(2, milestones_to_eol);
+
+  // Device is security only and milestones to EOL is n-2.
+  fake_prefs.SetString(kPrefsOmahaEolStatus, kEolStatusSecurityOnly);
+  fake_prefs.SetString(kPrefsOmahaMilestonesToEol, "2");
+  EXPECT_TRUE(
+      common_service_.GetEolStatus(&error_, &eol_status, &milestones_to_eol));
+  EXPECT_EQ(nullptr, error_);
+  EXPECT_EQ(EolStatus::kSecurityOnly, static_cast<EolStatus>(eol_status));
+  EXPECT_EQ(2, milestones_to_eol);
+}
+
+TEST_F(UpdateEngineServiceTest, GetEolStatusMilestonesToEolZero) {
+  FakePrefs fake_prefs;
+  fake_system_state_.set_prefs(&fake_prefs);
+  int32_t eol_status = static_cast<int32_t>(EolStatus::kEol);
+  MilestonesToEol milestones_to_eol = kMilestonesToEolNone;
+
+  // Device is EOL and milestones to EOL is n.
+  fake_prefs.SetString(kPrefsOmahaEolStatus, kEolStatusEol);
+  fake_prefs.SetString(kPrefsOmahaMilestonesToEol, "0");
+  EXPECT_TRUE(
+      common_service_.GetEolStatus(&error_, &eol_status, &milestones_to_eol));
+  EXPECT_EQ(nullptr, error_);
+  EXPECT_EQ(EolStatus::kEol, static_cast<EolStatus>(eol_status));
+  EXPECT_EQ(0, milestones_to_eol);
 }
 
 }  // namespace chromeos_update_engine
diff --git a/dbus_bindings/org.chromium.UpdateEngineInterface.dbus-xml b/dbus_bindings/org.chromium.UpdateEngineInterface.dbus-xml
index a183147..5671fde 100644
--- a/dbus_bindings/org.chromium.UpdateEngineInterface.dbus-xml
+++ b/dbus_bindings/org.chromium.UpdateEngineInterface.dbus-xml
@@ -139,6 +139,7 @@
     </method>
     <method name="GetEolStatus">
       <arg type="i" name="eol_status" direction="out" />
+      <arg type="i" name="milestones_to_eol" direction="out" />
     </method>
   </interface>
 </node>
diff --git a/dbus_service.cc b/dbus_service.cc
index b379603..168265d 100644
--- a/dbus_service.cc
+++ b/dbus_service.cc
@@ -212,8 +212,9 @@
 }
 
 bool DBusUpdateEngineService::GetEolStatus(ErrorPtr* error,
-                                           int32_t* out_eol_status) {
-  return common_->GetEolStatus(error, out_eol_status);
+                                           int32_t* out_eol_status,
+                                           int32_t* out_milestone_to_eol) {
+  return common_->GetEolStatus(error, out_eol_status, out_milestone_to_eol);
 }
 
 UpdateEngineAdaptor::UpdateEngineAdaptor(SystemState* system_state)
diff --git a/dbus_service.h b/dbus_service.h
index 2babf8c..9d79609 100644
--- a/dbus_service.h
+++ b/dbus_service.h
@@ -157,8 +157,13 @@
   bool GetLastAttemptError(brillo::ErrorPtr* error,
                            int32_t* out_last_attempt_error) override;
 
-  // Returns the current end-of-life status of the device in |out_eol_status|.
-  bool GetEolStatus(brillo::ErrorPtr* error, int32_t* out_eol_status) override;
+  // Returns the current EOL status of the device in |out_eol_status| and the
+  // milestones to EOL of the device in |out_milestones_to_eol| for EOL devices.
+  // In the case that milestones to EOL doesn't exists for EOL, it will default
+  // to |kMilestonesToEolNone|.
+  bool GetEolStatus(brillo::ErrorPtr* error,
+                    int32_t* out_eol_status,
+                    int32_t* out_milestones_to_eol) override;
 
  private:
   std::unique_ptr<UpdateEngineService> common_;
diff --git a/omaha_request_action.cc b/omaha_request_action.cc
index 6c67a3b..8da7e29 100644
--- a/omaha_request_action.cc
+++ b/omaha_request_action.cc
@@ -109,6 +109,7 @@
 
 // updatecheck attributes (without the underscore prefix).
 constexpr char kAttrEol[] = "eol";
+constexpr char kAttrMilestonesToEol[] = "milestones_to_eol";
 constexpr char kAttrRollback[] = "rollback";
 constexpr char kAttrFirmwareVersion[] = "firmware_version";
 constexpr char kAttrKernelVersion[] = "kernel_version";
@@ -1315,13 +1316,28 @@
 
 bool OmahaRequestAction::PersistEolStatus(const map<string, string>& attrs) {
   auto eol_attr = attrs.find(kAttrEol);
-  if (eol_attr != attrs.end()) {
-    return system_state_->prefs()->SetString(kPrefsOmahaEolStatus,
-                                             eol_attr->second);
-  } else if (system_state_->prefs()->Exists(kPrefsOmahaEolStatus)) {
-    return system_state_->prefs()->Delete(kPrefsOmahaEolStatus);
+  auto milestones_to_eol_attr = attrs.find(kAttrMilestonesToEol);
+
+  bool ret = true;
+  if (milestones_to_eol_attr == attrs.end()) {
+    system_state_->prefs()->Delete(kPrefsOmahaMilestonesToEol);
+    if (eol_attr != attrs.end()) {
+      LOG(WARNING) << "Milestones to EOL missing when EOL.";
+    }
+  } else if (!system_state_->prefs()->SetString(
+                 kPrefsOmahaMilestonesToEol, milestones_to_eol_attr->second)) {
+    LOG(ERROR) << "Setting milestones to EOL failed.";
+    ret = false;
   }
-  return true;
+
+  if (eol_attr == attrs.end()) {
+    system_state_->prefs()->Delete(kPrefsOmahaEolStatus);
+  } else if (!system_state_->prefs()->SetString(kPrefsOmahaEolStatus,
+                                                eol_attr->second)) {
+    LOG(ERROR) << "Setting EOL failed.";
+    ret = false;
+  }
+  return ret;
 }
 
 void OmahaRequestAction::ActionCompleted(ErrorCode code) {
diff --git a/omaha_request_action.h b/omaha_request_action.h
index 8dffb5c..0c256a8 100644
--- a/omaha_request_action.h
+++ b/omaha_request_action.h
@@ -144,6 +144,7 @@
               GetInstallDateWhenOOBECompletedWithValidDate);
   FRIEND_TEST(OmahaRequestActionTest,
               GetInstallDateWhenOOBECompletedDateChanges);
+  FRIEND_TEST(OmahaRequestActionTest, PersistEolStatusTest);
   friend class UpdateAttempterTest;
   FRIEND_TEST(UpdateAttempterTest, SessionIdTestEnforceEmptyStrPingOmaha);
   FRIEND_TEST(UpdateAttempterTest, SessionIdTestConsistencyInUpdateFlow);
@@ -188,8 +189,9 @@
   bool PersistCohortData(const std::string& prefs_key,
                          const std::string& new_value);
 
-  // Parse and persist the end-of-life status flag sent back in the updatecheck
-  // tag attributes. The flag will be validated and stored in the Prefs.
+  // Parse and persist the end-of-life status flag and milestones to EOL sent
+  // back in the updatecheck tag attributes. The flag will be validated and
+  // stored in the Prefs.
   bool PersistEolStatus(const std::map<std::string, std::string>& attrs);
 
   // If this is an update check request, initializes
diff --git a/omaha_request_action_unittest.cc b/omaha_request_action_unittest.cc
index 8008e00..11633b6 100644
--- a/omaha_request_action_unittest.cc
+++ b/omaha_request_action_unittest.cc
@@ -2012,6 +2012,65 @@
   EXPECT_EQ("security-only", eol_pref);
 }
 
+TEST_F(OmahaRequestActionTest, ParseUpdateCheckAttributesEolTest) {
+  tuc_params_.http_response =
+      "<?xml version=\"1.0\" encoding=\"UTF-8\"?><response "
+      "protocol=\"3.0\"><app appid=\"foo\" status=\"ok\">"
+      "<ping status=\"ok\"/><updatecheck status=\"noupdate\" "
+      "_eol=\"eol\" _milestones_to_eol=\"0\" _foo=\"bar\"/></app></response>";
+  tuc_params_.expected_check_result = metrics::CheckResult::kNoUpdateAvailable;
+  tuc_params_.expected_check_reaction = metrics::CheckReaction::kUnset;
+
+  ASSERT_TRUE(TestUpdateCheck());
+
+  string eol_pref, milestones_to_eol_pref;
+  EXPECT_TRUE(
+      fake_system_state_.prefs()->GetString(kPrefsOmahaEolStatus, &eol_pref));
+  EXPECT_EQ("eol", eol_pref);
+  EXPECT_TRUE(fake_system_state_.prefs()->GetString(kPrefsOmahaMilestonesToEol,
+                                                    &milestones_to_eol_pref));
+  EXPECT_EQ("0", milestones_to_eol_pref);
+}
+
+TEST_F(OmahaRequestActionTest,
+       ParseUpdateCheckAttributesMissingMilestonesToEolTest) {
+  tuc_params_.http_response =
+      "<?xml version=\"1.0\" encoding=\"UTF-8\"?><response "
+      "protocol=\"3.0\"><app appid=\"foo\" status=\"ok\">"
+      "<ping status=\"ok\"/><updatecheck status=\"noupdate\" "
+      "_eol=\"eol\"/></app></response>";
+  tuc_params_.expected_check_result = metrics::CheckResult::kNoUpdateAvailable;
+  tuc_params_.expected_check_reaction = metrics::CheckReaction::kUnset;
+
+  ASSERT_TRUE(TestUpdateCheck());
+
+  string eol_pref, milestones_to_eol_pref;
+  EXPECT_TRUE(
+      fake_system_state_.prefs()->GetString(kPrefsOmahaEolStatus, &eol_pref));
+  EXPECT_EQ("eol", eol_pref);
+  EXPECT_FALSE(fake_system_state_.prefs()->Exists(kPrefsOmahaMilestonesToEol));
+}
+
+TEST_F(OmahaRequestActionTest, ParseUpdateCheckAttributesMilestonesToEolTest) {
+  tuc_params_.http_response =
+      "<?xml version=\"1.0\" encoding=\"UTF-8\"?><response "
+      "protocol=\"3.0\"><app appid=\"foo\" status=\"ok\">"
+      "<ping status=\"ok\"/><updatecheck status=\"noupdate\" "
+      "_eol=\"supported\" _milestones_to_eol=\"3\"/></app></response>";
+  tuc_params_.expected_check_result = metrics::CheckResult::kNoUpdateAvailable;
+  tuc_params_.expected_check_reaction = metrics::CheckReaction::kUnset;
+
+  ASSERT_TRUE(TestUpdateCheck());
+
+  string eol_pref, milestones_to_eol_pref;
+  EXPECT_TRUE(
+      fake_system_state_.prefs()->GetString(kPrefsOmahaEolStatus, &eol_pref));
+  EXPECT_EQ("supported", eol_pref);
+  EXPECT_TRUE(fake_system_state_.prefs()->GetString(kPrefsOmahaMilestonesToEol,
+                                                    &milestones_to_eol_pref));
+  EXPECT_EQ("3", milestones_to_eol_pref);
+}
+
 TEST_F(OmahaRequestActionTest, NoUniqueIDTest) {
   tuc_params_.http_response = "invalid xml>";
   tuc_params_.expected_code = ErrorCode::kOmahaRequestXMLParseError;
diff --git a/omaha_utils.cc b/omaha_utils.cc
index 6bd7525..dffa5d3 100644
--- a/omaha_utils.cc
+++ b/omaha_utils.cc
@@ -17,17 +17,15 @@
 #include "update_engine/omaha_utils.h"
 
 #include <base/logging.h>
+#include <base/strings/string_number_conversions.h>
 
 namespace chromeos_update_engine {
 
-namespace {
-
-// The possible string values for the end-of-life status.
 const char kEolStatusSupported[] = "supported";
 const char kEolStatusSecurityOnly[] = "security-only";
 const char kEolStatusEol[] = "eol";
 
-}  // namespace
+const MilestonesToEol kMilestonesToEolNone = -1;
 
 const char* EolStatusToString(EolStatus eol_status) {
   switch (eol_status) {
@@ -50,8 +48,23 @@
     return EolStatus::kSecurityOnly;
   if (eol_status == kEolStatusEol)
     return EolStatus::kEol;
-  LOG(WARNING) << "Invalid end-of-life attribute: " << eol_status;
+  LOG(WARNING) << "Invalid EOL attribute: " << eol_status;
   return EolStatus::kSupported;
 }
 
+std::string MilestonesToEolToString(MilestonesToEol milestones_to_eol) {
+  return base::IntToString(milestones_to_eol);
+}
+
+MilestonesToEol StringToMilestonesToEol(const std::string& milestones_to_eol) {
+  MilestonesToEol milestone = kMilestonesToEolNone;
+  if (!base::StringToInt(milestones_to_eol, &milestone)) {
+    LOG(WARNING) << "Invalid milestones to EOL attribute: "
+                 << milestones_to_eol;
+    return kMilestonesToEolNone;
+  }
+
+  return milestone;
+}
+
 }  // namespace chromeos_update_engine
diff --git a/omaha_utils.h b/omaha_utils.h
index 8614540..6000451 100644
--- a/omaha_utils.h
+++ b/omaha_utils.h
@@ -21,6 +21,11 @@
 
 namespace chromeos_update_engine {
 
+// The possible string values for the end-of-life status.
+extern const char kEolStatusSupported[];
+extern const char kEolStatusSecurityOnly[];
+extern const char kEolStatusEol[];
+
 // The end-of-life status of the device.
 enum class EolStatus {
   kSupported = 0,
@@ -28,6 +33,10 @@
   kEol,
 };
 
+using MilestonesToEol = int;
+// The default milestones to EOL.
+extern const MilestonesToEol kMilestonesToEolNone;
+
 // Returns the string representation of the |eol_status|.
 const char* EolStatusToString(EolStatus eol_status);
 
@@ -35,6 +44,14 @@
 // of an invalid string, the default "supported" value will be used instead.
 EolStatus StringToEolStatus(const std::string& eol_status);
 
+// Returns the string representation of the |milestones_to_eol|.
+std::string MilestonesToEolToString(int milestones_to_eol);
+
+// Converts the milestones to EOL string to an |MilestonesToEol| enum class.
+// When the milestones to EOL is not an integer, the default
+// |kMilestonesToEolNone| will be returned.
+MilestonesToEol StringToMilestonesToEol(const std::string& milestones_to_eol);
+
 }  // namespace chromeos_update_engine
 
 #endif  // UPDATE_ENGINE_OMAHA_UTILS_H_
diff --git a/omaha_utils_unittest.cc b/omaha_utils_unittest.cc
index 8ceb76b..59c0366 100644
--- a/omaha_utils_unittest.cc
+++ b/omaha_utils_unittest.cc
@@ -39,4 +39,12 @@
   EXPECT_EQ(EolStatus::kSupported, StringToEolStatus("hello, world!"));
 }
 
+TEST(OmahaUtilsTest, MilestonesToEolTest) {
+  EXPECT_EQ(kMilestonesToEolNone, StringToMilestonesToEol(""));
+  EXPECT_EQ(kMilestonesToEolNone, StringToMilestonesToEol("not_a_number"));
+  EXPECT_EQ(1, StringToMilestonesToEol("1"));
+  EXPECT_EQ(0, StringToMilestonesToEol("0"));
+  EXPECT_EQ(-1, StringToMilestonesToEol("-1"));
+}
+
 }  // namespace chromeos_update_engine
diff --git a/update_engine_client.cc b/update_engine_client.cc
index 954e856..9748c4d 100644
--- a/update_engine_client.cc
+++ b/update_engine_client.cc
@@ -41,6 +41,7 @@
 
 using chromeos_update_engine::EolStatus;
 using chromeos_update_engine::ErrorCode;
+using chromeos_update_engine::MilestonesToEol;
 using chromeos_update_engine::UpdateEngineStatusToString;
 using chromeos_update_engine::UpdateStatusToString;
 using chromeos_update_engine::utils::ErrorCodeToString;
@@ -559,12 +560,20 @@
   }
 
   if (FLAGS_eol_status) {
-    int eol_status;
-    if (!client_->GetEolStatus(&eol_status)) {
-      LOG(ERROR) << "Error getting the end-of-life status.";
+    int eol_status, milestones_to_eol;
+    if (!client_->GetEolStatus(&eol_status, &milestones_to_eol)) {
+      LOG(ERROR) << "Error getting the end-of-life status and milestones to "
+                    "end-of-life.";
     } else {
       EolStatus eol_status_code = static_cast<EolStatus>(eol_status);
-      printf("EOL_STATUS=%s\n", EolStatusToString(eol_status_code));
+      MilestonesToEol milestones_to_eol_code = milestones_to_eol;
+      printf(
+          "EOL_STATUS=%s\n"
+          "MILESTONES_TO_EOL=%s\n",
+          EolStatusToString(eol_status_code),
+          chromeos_update_engine::MilestonesToEolToString(
+              milestones_to_eol_code)
+              .c_str());
     }
   }