update_engine: add utilities for update staging policy
Add staging_utils files that contain logic to decide what
to changes should be done to the state. This includes:
* Turn off staging if there is a forced update or if OOBE hasn't been
completed
* Keep the current staging state if no changes to the policy are
detected.
* Use a new waiting time if there have been changes to the policy.
* If there is a persisted value that is still valid, use it.
These changes aren't added to the update engine yet. The return value
of CalculateStagingCase will be used in update_attempter in
a switch statement to update the state based on the value.
BUG=chromium:858621
TEST=cros_workon_make update_engine --test
CQ-DEPEND=CL:1142480
Change-Id: Ib06365793618c3f2a357e3ace8c660fe51cdf950
Reviewed-on: https://chromium-review.googlesource.com/1138983
Commit-Ready: ChromeOS CL Exonerator Bot <chromiumos-cl-exonerator@appspot.gserviceaccount.com>
Tested-by: Adolfo Higueros <adokar@google.com>
Reviewed-by: Amin Hassani <ahassani@chromium.org>
diff --git a/Android.mk b/Android.mk
index 2fc6f46..d1d8488 100644
--- a/Android.mk
+++ b/Android.mk
@@ -335,6 +335,7 @@
update_manager/real_system_provider.cc \
update_manager/real_time_provider.cc \
update_manager/real_updater_provider.cc \
+ update_manager/staging_utils.cc \
update_manager/state_factory.cc \
update_manager/update_manager.cc \
update_manager/update_time_restrictions_policy_impl.cc \
@@ -1021,6 +1022,7 @@
update_manager/real_system_provider_unittest.cc \
update_manager/real_time_provider_unittest.cc \
update_manager/real_updater_provider_unittest.cc \
+ update_manager/staging_utils_unittest.cc \
update_manager/umtest_utils.cc \
update_manager/update_manager_unittest.cc \
update_manager/update_time_restrictions_policy_impl_unittest.cc \
diff --git a/update_engine.gyp b/update_engine.gyp
index 20fb282..cbdce2b 100644
--- a/update_engine.gyp
+++ b/update_engine.gyp
@@ -293,6 +293,7 @@
'update_manager/real_system_provider.cc',
'update_manager/real_time_provider.cc',
'update_manager/real_updater_provider.cc',
+ 'update_manager/staging_utils.cc',
'update_manager/state_factory.cc',
'update_manager/update_manager.cc',
'update_manager/update_time_restrictions_policy_impl.cc',
@@ -581,6 +582,7 @@
'update_manager/real_system_provider_unittest.cc',
'update_manager/real_time_provider_unittest.cc',
'update_manager/real_updater_provider_unittest.cc',
+ 'update_manager/staging_utils_unittest.cc',
'update_manager/umtest_utils.cc',
'update_manager/update_manager_unittest.cc',
'update_manager/update_time_restrictions_policy_impl_unittest.cc',
diff --git a/update_manager/staging_utils.cc b/update_manager/staging_utils.cc
new file mode 100644
index 0000000..4835ab2
--- /dev/null
+++ b/update_manager/staging_utils.cc
@@ -0,0 +1,142 @@
+//
+// Copyright (C) 2018 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/update_manager/staging_utils.h"
+
+#include <utility>
+#include <vector>
+
+#include <base/logging.h>
+#include <base/rand_util.h>
+#include <base/time/time.h>
+#include <policy/device_policy.h>
+
+#include "update_engine/common/constants.h"
+#include "update_engine/common/hardware_interface.h"
+#include "update_engine/common/prefs_interface.h"
+#include "update_engine/system_state.h"
+
+using base::TimeDelta;
+using chromeos_update_engine::kPrefsWallClockStagingWaitPeriod;
+using chromeos_update_engine::PrefsInterface;
+using chromeos_update_engine::SystemState;
+using policy::DevicePolicy;
+
+namespace chromeos_update_manager {
+
+int GetStagingSchedule(const DevicePolicy* device_policy,
+ StagingSchedule* staging_schedule_out) {
+ StagingSchedule staging_schedule;
+ if (!device_policy->GetDeviceUpdateStagingSchedule(&staging_schedule) ||
+ staging_schedule.empty()) {
+ return 0;
+ }
+
+ // Last percentage of the schedule should be 100.
+ if (staging_schedule.back().percentage != 100) {
+ LOG(ERROR) << "Last percentage of the schedule is not 100, it's: "
+ << staging_schedule.back().percentage;
+ return 0;
+ }
+
+ int previous_days = 0;
+ int previous_percentage = -1;
+ // Ensure that the schedule has a monotonically increasing set of percentages
+ // and that days are also monotonically increasing.
+ for (const auto& staging_pair : staging_schedule) {
+ int days = staging_pair.days;
+ if (previous_days >= days) {
+ LOG(ERROR) << "Days in staging schedule are not monotonically "
+ << "increasing. Previous value: " << previous_days
+ << " Current value: " << days;
+ return 0;
+ }
+ previous_days = days;
+ int percentage = staging_pair.percentage;
+ if (previous_percentage >= percentage) {
+ LOG(ERROR) << "Percentages in staging schedule are not monotonically "
+ << "increasing. Previous value: " << previous_percentage
+ << " Current value: " << percentage;
+ return 0;
+ }
+ previous_percentage = percentage;
+ }
+ // Modify staging schedule only if the schedule in the device policy is valid.
+ if (staging_schedule_out)
+ *staging_schedule_out = std::move(staging_schedule);
+
+ return previous_days;
+}
+
+int CalculateWaitTimeInDaysFromSchedule(
+ const StagingSchedule& staging_schedule) {
+ int prev_days = 0;
+ int percentage_position = base::RandInt(1, 100);
+ for (const auto& staging_pair : staging_schedule) {
+ int days = staging_pair.days;
+ if (percentage_position <= staging_pair.percentage) {
+ // Scatter between the start of the range and the end.
+ return prev_days + base::RandInt(1, days - prev_days);
+ }
+ prev_days = days;
+ }
+ // Something went wrong.
+ NOTREACHED();
+ return 0;
+}
+
+StagingCase CalculateStagingCase(const DevicePolicy* device_policy,
+ PrefsInterface* prefs,
+ TimeDelta* staging_wait_time,
+ StagingSchedule* staging_schedule) {
+ // Check that the schedule in the device policy is correct.
+ StagingSchedule new_staging_schedule;
+ int max_days = GetStagingSchedule(device_policy, &new_staging_schedule);
+ if (max_days == 0)
+ return StagingCase::kOff;
+
+ // Calculate the new wait time.
+ TimeDelta new_staging_wait_time = TimeDelta::FromDays(
+ CalculateWaitTimeInDaysFromSchedule(new_staging_schedule));
+ DCHECK_GT(new_staging_wait_time.InSeconds(), 0);
+ if (staging_wait_time->InSeconds() > 0) {
+ // If there hasn't been any changes to the schedule and there is a value
+ // set, don't change the waiting time.
+ if (new_staging_schedule == *staging_schedule) {
+ return StagingCase::kNoAction;
+ }
+ // Otherwise, update the schedule and wait time.
+ *staging_wait_time = new_staging_wait_time;
+ *staging_schedule = std::move(new_staging_schedule);
+ return StagingCase::kNoSavedValue;
+ }
+ // Getting this means the schedule changed, update the old schedule.
+ *staging_schedule = std::move(new_staging_schedule);
+
+ int64_t wait_period_in_days;
+ // There exists a persisted value that is valid. That is, it's smaller than
+ // the maximum amount of days of staging set by the user.
+ if (prefs->GetInt64(kPrefsWallClockStagingWaitPeriod, &wait_period_in_days) &&
+ wait_period_in_days > 0 && wait_period_in_days <= max_days) {
+ *staging_wait_time = TimeDelta::FromDays(wait_period_in_days);
+ return StagingCase::kSetStagingFromPref;
+ }
+
+ *staging_wait_time = new_staging_wait_time;
+ return StagingCase::kNoSavedValue;
+}
+
+} // namespace chromeos_update_manager
diff --git a/update_manager/staging_utils.h b/update_manager/staging_utils.h
new file mode 100644
index 0000000..e91bfeb
--- /dev/null
+++ b/update_manager/staging_utils.h
@@ -0,0 +1,71 @@
+//
+// Copyright (C) 2018 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_UPDATE_MANAGER_STAGING_UTILS_H_
+#define UPDATE_ENGINE_UPDATE_MANAGER_STAGING_UTILS_H_
+
+#include <utility>
+#include <vector>
+
+#include <base/time/time.h>
+#include <policy/device_policy.h>
+
+#include "update_engine/common/prefs_interface.h"
+
+namespace chromeos_update_manager {
+
+using StagingSchedule = std::vector<policy::DevicePolicy::DayPercentagePair>;
+
+// Possible cases that staging might run into based on the inputs.
+enum class StagingCase {
+ // Staging is off, remove the persisted value.
+ kOff,
+ // Staging is enabled, but there is no valid persisted value, saved value or
+ // the value of the schedule has changed.
+ kNoSavedValue,
+ // Staging is enabled, and there is a valid persisted value.
+ kSetStagingFromPref,
+ // Staging is enabled, and there have been no changes to the schedule.
+ kNoAction
+};
+
+// Calculate the bucket in which the device belongs based on a given staging
+// schedule. |staging_schedule| is assumed to have already been validated.
+int CalculateWaitTimeInDaysFromSchedule(
+ const StagingSchedule& staging_schedule);
+
+// Verifies that |device_policy| contains a valid staging schedule. If
+// |device_policy| contains a valid staging schedule, move it into
+// |staging_schedule_out| and return the total number of days spanned by the
+// schedule. Otherwise, don't modify |staging_schedule_out| and return 0 (which
+// is an invalid value for the length of a schedule).
+int GetStagingSchedule(const policy::DevicePolicy* device_policy,
+ StagingSchedule* staging_schedule_out);
+
+// Uses the given arguments to check whether staging is on, and whether the
+// state should be updated with a new waiting time or not. |staging_wait_time|
+// should contain the old value of the wait time, it will be replaced with the
+// new calculated wait time value if staging is on. |staging_schedule| should
+// contain the previous staging schedule, if there is a new schedule found, its
+// value will be replaced with the new one.
+StagingCase CalculateStagingCase(const policy::DevicePolicy* device_policy,
+ chromeos_update_engine::PrefsInterface* prefs,
+ base::TimeDelta* staging_wait_time,
+ StagingSchedule* staging_schedule);
+
+} // namespace chromeos_update_manager
+
+#endif // UPDATE_ENGINE_UPDATE_MANAGER_STAGING_UTILS_H_
diff --git a/update_manager/staging_utils_unittest.cc b/update_manager/staging_utils_unittest.cc
new file mode 100644
index 0000000..8d75acd
--- /dev/null
+++ b/update_manager/staging_utils_unittest.cc
@@ -0,0 +1,175 @@
+//
+// Copyright (C) 2018 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/update_manager/staging_utils.h"
+
+#include <memory>
+#include <utility>
+
+#include <base/time/time.h>
+#include <gtest/gtest.h>
+#include <policy/mock_device_policy.h>
+
+#include "update_engine/common/constants.h"
+#include "update_engine/common/fake_prefs.h"
+
+using base::TimeDelta;
+using chromeos_update_engine::FakePrefs;
+using chromeos_update_engine::kPrefsWallClockStagingWaitPeriod;
+using testing::_;
+using testing::DoAll;
+using testing::Return;
+using testing::SetArgPointee;
+
+namespace chromeos_update_manager {
+
+constexpr TimeDelta kDay = TimeDelta::FromDays(1);
+constexpr int kMaxDays = 28;
+constexpr int kValidDaySum = 14;
+const StagingSchedule valid_schedule = {{2, 0}, {7, 50}, {9, 80}, {14, 100}};
+
+class StagingUtilsScheduleTest : public testing::Test {
+ protected:
+ void SetUp() override {
+ test_wait_time_ = TimeDelta();
+ test_staging_schedule_ = StagingSchedule();
+ }
+
+ void SetStagingSchedule(const StagingSchedule& staging_schedule) {
+ EXPECT_CALL(device_policy_, GetDeviceUpdateStagingSchedule(_))
+ .WillRepeatedly(
+ DoAll(SetArgPointee<0>(staging_schedule), Return(true)));
+ }
+
+ void SetPersistedStagingVal(int64_t wait_time) {
+ EXPECT_TRUE(
+ fake_prefs_.SetInt64(kPrefsWallClockStagingWaitPeriod, wait_time));
+ }
+
+ void TestStagingCase(const StagingCase& expected) {
+ EXPECT_EQ(expected,
+ CalculateStagingCase(&device_policy_,
+ &fake_prefs_,
+ &test_wait_time_,
+ &test_staging_schedule_));
+ }
+
+ void ExpectNoChanges() {
+ EXPECT_EQ(TimeDelta(), test_wait_time_);
+ EXPECT_EQ(StagingSchedule(), test_staging_schedule_);
+ }
+
+ policy::MockDevicePolicy device_policy_;
+ TimeDelta test_wait_time_;
+ StagingSchedule test_staging_schedule_;
+ FakePrefs fake_prefs_;
+};
+
+// Last element should be 100, if not return false.
+TEST_F(StagingUtilsScheduleTest, GetStagingScheduleInvalidLastElem) {
+ SetStagingSchedule(StagingSchedule{{2, 10}, {4, 20}, {5, 40}});
+ EXPECT_EQ(0, GetStagingSchedule(&device_policy_, &test_staging_schedule_));
+ ExpectNoChanges();
+}
+
+// Percentage should be monotonically increasing.
+TEST_F(StagingUtilsScheduleTest, GetStagingScheduleNonMonotonic) {
+ SetStagingSchedule(StagingSchedule{{2, 10}, {6, 20}, {11, 20}, {12, 100}});
+ EXPECT_EQ(0, GetStagingSchedule(&device_policy_, &test_staging_schedule_));
+ ExpectNoChanges();
+}
+
+// The days should be monotonically increasing.
+TEST_F(StagingUtilsScheduleTest, GetStagingScheduleOverMaxDays) {
+ SetStagingSchedule(StagingSchedule{{2, 10}, {4, 20}, {15, 30}, {10, 100}});
+ EXPECT_EQ(0, GetStagingSchedule(&device_policy_, &test_staging_schedule_));
+ ExpectNoChanges();
+}
+
+TEST_F(StagingUtilsScheduleTest, GetStagingScheduleValid) {
+ SetStagingSchedule(valid_schedule);
+ EXPECT_EQ(kValidDaySum,
+ GetStagingSchedule(&device_policy_, &test_staging_schedule_));
+ EXPECT_EQ(test_staging_schedule_, valid_schedule);
+}
+
+TEST_F(StagingUtilsScheduleTest, StagingOffNoSchedule) {
+ // If the function returns false, the schedule shouldn't get used.
+ EXPECT_CALL(device_policy_, GetDeviceUpdateStagingSchedule(_))
+ .WillRepeatedly(DoAll(SetArgPointee<0>(valid_schedule), Return(false)));
+ TestStagingCase(StagingCase::kOff);
+ ExpectNoChanges();
+}
+
+TEST_F(StagingUtilsScheduleTest, StagingOffEmptySchedule) {
+ SetStagingSchedule(StagingSchedule());
+ TestStagingCase(StagingCase::kOff);
+ ExpectNoChanges();
+}
+
+TEST_F(StagingUtilsScheduleTest, StagingOffInvalidSchedule) {
+ // Any invalid schedule should return |StagingCase::kOff|.
+ SetStagingSchedule(StagingSchedule{{3, 30}, {6, 40}});
+ TestStagingCase(StagingCase::kOff);
+ ExpectNoChanges();
+}
+
+TEST_F(StagingUtilsScheduleTest, StagingOnNoAction) {
+ test_wait_time_ = kDay;
+ // Same as valid schedule, just using std::pair types.
+ StagingSchedule valid_schedule_pairs = {{2, 0}, {7, 50}, {9, 80}, {14, 100}};
+ test_staging_schedule_ = valid_schedule_pairs;
+ SetStagingSchedule(valid_schedule);
+ TestStagingCase(StagingCase::kNoAction);
+ // Vars should not be changed.
+ EXPECT_EQ(kDay, test_wait_time_);
+ EXPECT_EQ(test_staging_schedule_, valid_schedule_pairs);
+}
+
+TEST_F(StagingUtilsScheduleTest, StagingNoSavedValueChangePolicy) {
+ test_wait_time_ = kDay;
+ SetStagingSchedule(valid_schedule);
+ TestStagingCase(StagingCase::kNoSavedValue);
+ // Vars should change since < 2 days should not be possible due to
+ // valid_schedule's value.
+ EXPECT_NE(kDay, test_wait_time_);
+ EXPECT_EQ(test_staging_schedule_, valid_schedule);
+ EXPECT_LE(test_wait_time_, kDay * kMaxDays);
+}
+
+// Tests the case where there was a reboot and there is no persisted value.
+TEST_F(StagingUtilsScheduleTest, StagingNoSavedValueNoPersisted) {
+ SetStagingSchedule(valid_schedule);
+ TestStagingCase(StagingCase::kNoSavedValue);
+ // Vars should change since there are no preset values and there is a new
+ // staging schedule.
+ EXPECT_NE(TimeDelta(), test_wait_time_);
+ EXPECT_EQ(test_staging_schedule_, valid_schedule);
+ EXPECT_LE(test_wait_time_, kDay * kMaxDays);
+}
+
+// If there is a pref set and its value is less than the day count, use that
+// pref.
+TEST_F(StagingUtilsScheduleTest, StagingSetFromPref) {
+ SetStagingSchedule(valid_schedule);
+ SetPersistedStagingVal(5);
+ TestStagingCase(StagingCase::kSetStagingFromPref);
+ // Vars should change.
+ EXPECT_EQ(kDay * 5, test_wait_time_);
+ EXPECT_EQ(test_staging_schedule_, valid_schedule);
+}
+
+} // namespace chromeos_update_manager