blob: 7c282e794481a3bf0b73e18a445b64d472d56dd5 [file] [log] [blame]
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package policy
import (
"context"
"encoding/json"
"path/filepath"
"time"
"github.com/golang/protobuf/ptypes/empty"
"chromiumos/tast/common/policy"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/lsbrelease"
"chromiumos/tast/remote/policyutil"
"chromiumos/tast/rpc"
aupb "chromiumos/tast/services/cros/autoupdate"
ppb "chromiumos/tast/services/cros/policy"
"chromiumos/tast/ssh/linuxssh"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: RollbackWithOmaha,
LacrosStatus: testing.LacrosVariantUnneeded,
Desc: "Example test for the enterprise rollback update",
Contacts: []string{
"gabormagda@google.com", // Test author
"chromeos-commercial-remote-management@google.com",
},
Attr: []string{}, // Manual execution only.
VarDeps: []string{"policy.RollbackWithOmaha.confirm", "policy.RollbackWithOmaha.sourceVersion", "policy.RollbackWithOmaha.targetVersion"},
SoftwareDeps: []string{"reboot", "chrome"},
ServiceDeps: []string{"tast.cros.policy.PolicyService", "tast.cros.autoupdate.UpdateService"},
Timeout: 5 * time.Minute,
})
}
// RollbackWithOmaha test must be provided the source and target image versions.
// The source version should be a full version string. The target can be
// just a prefix. Furthermore, test should be started with
// -var=policy.RollbackWithOmaha.confirm=ICanRollbackMyDUT
// to avoid accidental execution of the test.
//
// For example, to run a rollback from M96 to M94:
// tast run
// -var=policy.RollbackWithOmaha.confirm=ICanRollbackMyDUT
// -var=policy.RollbackWithOmaha.sourceVersion=14244.0.0
// -var=policy.RollbackWithOmaha.targetVersion=14092.
// <ip> policy.RollbackWithOmaha
func RollbackWithOmaha(ctx context.Context, s *testing.State) {
if s.RequiredVar("policy.RollbackWithOmaha.confirm") != "ICanRollbackMyDUT" {
s.Log("You should only run this example test if you have manual access to your DUT")
s.Log("After the update, you can restore the previous partition with the following command:")
s.Log("\tupdate_engine_client --rollback --nopowerwash")
s.Fatal("Failed to make sure it is an intentional test execution")
}
successfulUpdate := false
func(ctx context.Context) {
defer func(ctx context.Context) {
if !successfulUpdate {
if err := policyutil.EnsureTPMAndSystemStateAreResetRemote(ctx, s.DUT()); err != nil {
s.Error("Failed to reset TPM after test: ", err)
}
}
}(ctx)
cleanupCtx := ctx
ctx, cancel := ctxutil.Shorten(ctx, 15*time.Second)
defer cancel()
// Reset TPM.
if err := policyutil.EnsureTPMAndSystemStateAreResetRemote(ctx, s.DUT()); err != nil {
s.Fatal("Failed to reset TPM: ", err)
}
// Connect to DUT.
cl, err := rpc.Dial(ctx, s.DUT(), s.RPCHint())
if err != nil {
s.Fatal("Failed to connect to the RPC service on the DUT: ", err)
}
defer cl.Close(cleanupCtx)
// Create clients.
policyClient := ppb.NewPolicyServiceClient(cl.Conn)
updateClient := aupb.NewUpdateServiceClient(cl.Conn)
// Create an empty /mnt/stateful_partition/etc/lsb-release if it doesn't
// exist yet.
if err := s.DUT().Conn().CommandContext(ctx, "touch", "/mnt/stateful_partition/etc/lsb-release").Run(); err != nil {
s.Error("Failed to touch stateful lsb-release: ", err)
}
// Enable the DUT to receive updates.
originalContent, err := signBoardName(ctx, updateClient)
if err != nil {
s.Fatal("Failed to enable the DUT to receive updates: ", err)
}
defer func(ctx context.Context, lsbContent []byte) {
if _, err := updateClient.OverwriteStatefulLSBRelease(ctx, &aupb.LSBRelease{ContentJson: lsbContent}); err != nil {
s.Log("Failed to restore lsb-release in the stateful partition: ", err)
}
}(cleanupCtx, originalContent)
// Enroll DUT.
pJSON, err := json.Marshal(policy.NewBlob())
if err != nil {
s.Fatal("Failed to serialize policies: ", err)
}
if _, err := policyClient.EnrollUsingChrome(ctx, &ppb.EnrollUsingChromeRequest{
PolicyJson: pJSON,
}); err != nil {
s.Fatal("Failed to enroll using chrome: ", err)
}
defer policyClient.StopChromeAndFakeDMS(ctx, &empty.Empty{})
targetVersion := s.RequiredVar("policy.RollbackWithOmaha.targetVersion")
// Set update policies.
rollbackPolicies := []policy.Policy{
// Note: the update will fail if the other partition already has the same image
// that is selected below to rollback to.
&policy.DeviceTargetVersionPrefix{Val: targetVersion}, // Pass by argument, e.g. "13982." for M92.
&policy.DeviceRollbackAllowedMilestones{Val: 4},
&policy.DeviceRollbackToTargetVersion{Val: 3}, // Roll back and stay on target version if OS version is newer than target. Try to carry over device-level configuration.
&policy.ChromeOsReleaseChannel{Val: "stable-channel"},
&policy.ChromeOsReleaseChannelDelegated{Val: false},
}
policyBlob := policy.NewBlob()
policyBlob.AddPolicies(rollbackPolicies)
pJSON, err = json.Marshal(policyBlob)
if err != nil {
s.Fatal("Failed to serialize policies: ", err)
}
if _, err := policyClient.UpdatePolicies(ctx, &ppb.UpdatePoliciesRequest{
PolicyJson: pJSON,
}); err != nil {
s.Fatal("Failed to enroll using chrome: ", err)
}
// Get the update log files even if the update fails.
defer func(ctx context.Context) {
if err := linuxssh.GetFile(ctx, s.DUT().Conn(), "/var/log/update_engine.log", filepath.Join(s.OutDir(), "update_engine.log"), linuxssh.DereferenceSymlinks); err != nil {
s.Log("Failed to save update engine log: ", err)
}
}(cleanupCtx)
sourceVersion := s.RequiredVar("policy.RollbackWithOmaha.sourceVersion")
// Update DUT with an update from the official prod server.
// The server is given explicitly because self-built images may not have
// it configured in their lsb-release file.
if _, err := updateClient.CheckForUpdate(ctx, &aupb.UpdateRequest{
OmahaUrl: "https://tools.google.com/service/update2",
AppVersion: sourceVersion,
}); err != nil {
s.Fatal("Failed to check for updates: ", err)
}
successfulUpdate = true
}(ctx)
// Reboot the DUT.
if successfulUpdate {
s.Log("Update was successful, rebooting DUT")
s.Log("Note: The DUT will remain enrolled after reboot")
s.Log("Note: After the reboot the SSH connecton to the DUT is disabled,")
s.Log(" manual restoration is required: update_engine_client --rollback --nopowerwash")
rebootCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
// Restart in an independent process, so the SSH connection can be closed before the restart.
s.DUT().Conn().CommandContext(rebootCtx, "nohup", "bash", "-c", "sleep 15; reboot;").Run() // Ignore the error.
}
}
// signBoardName adds an entry to /mnt/stateful_partition/etc/lsb-release with a signed board name
// to enable the DUT to receive updates.
// Returns with the original content of /mnt/stateful_partition/etc/lsb-release so it can be restored after the update.
func signBoardName(ctx context.Context, client aupb.UpdateServiceClient) ([]byte, error) {
// Get board name from /etc/lsb-release.
response, err := client.LSBReleaseContent(ctx, &empty.Empty{})
if err != nil {
return nil, errors.Wrap(err, "failed to read lsb-release")
}
var lsb map[string]string
if err := json.Unmarshal(response.ContentJson, &lsb); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal lsb-relese content")
}
board, ok := lsb[lsbrelease.Board]
if !ok {
return nil, errors.New("failed to determine DUT board")
}
signedBoardName := board + "-signed-mp-v3keys"
// Get content of /mnt/stateful_partition/etc/lsb-release.
response, err = client.StatefulLSBReleaseContent(ctx, &empty.Empty{})
if err != nil {
return nil, errors.Wrap(err, "failed to read lsb-release on the stateful partition")
}
testing.ContextLogf(ctx, "Adding the %q board name to lsb-release in the stateful partition", signedBoardName)
var statefulLsb map[string]string
if err := json.Unmarshal(response.ContentJson, &statefulLsb); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal stateful lsb-relese content")
}
statefulLsb[lsbrelease.Board] = signedBoardName
newStatefulLsbJSON, err := json.Marshal(statefulLsb)
if err != nil {
return nil, errors.Wrap(err, "failed to serialize stateful lsb-release content")
}
if _, err := client.OverwriteStatefulLSBRelease(ctx, &aupb.LSBRelease{ContentJson: newStatefulLsbJSON}); err != nil {
return nil, errors.Wrap(err, "failed to overwrite lsb-release in the stateful partition")
}
return response.ContentJson, nil
}