blob: 6f788a72ccef78d5bdb19fbcf968ca53d55dc2c7 [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Container for the CrOSProvision state machine
package service
import (
common_utils "go.chromium.org/chromiumos/test/provision/v2/common-utils"
"go.chromium.org/chromiumos/test/provision/v2/common-utils/metadata"
"context"
"fmt"
conf "go.chromium.org/chromiumos/config/go"
"go.chromium.org/chromiumos/config/go/test/api"
lab_api "go.chromium.org/chromiumos/config/go/test/lab/api"
)
// CrOSService inherits ServiceInterface
type CrOSService struct {
Connection common_utils.ServiceAdapterInterface
MachineMetadata metadata.MachineMetadata
ImagePath *conf.StoragePath
OverwritePayload *conf.StoragePath
PreserveStateful bool
DlcSpecs []*api.CrOSProvisionMetadata_DLCSpec
UpdateFirmware bool
UpdateCros bool
QuickResetDevice bool
}
func NewCrOSService(dut *lab_api.Dut, dutClient api.DutServiceClient, req *api.InstallRequest) (*CrOSService, error) {
m, err := unpackMetadata(req)
if err != nil {
return nil, err
}
return &CrOSService{
Connection: common_utils.NewServiceAdapter(dutClient, req.GetPreventReboot()),
ImagePath: req.ImagePath,
OverwritePayload: req.OverwritePayload,
PreserveStateful: m.PreserveStateful,
DlcSpecs: m.DlcSpecs,
MachineMetadata: metadata.MachineMetadata{},
UpdateFirmware: m.UpdateFirmware,
UpdateCros: true, // Force this true by default
}, nil
}
func NewCrOSServiceFromCrOSProvisionRequest(dutClient api.DutServiceClient, req *api.CrosProvisionRequest) *CrOSService {
var dlcSpecs []*api.CrOSProvisionMetadata_DLCSpec
for _, id := range req.GetProvisionState().GetSystemImage().GetDlcs() {
dlcSpec := &api.CrOSProvisionMetadata_DLCSpec{
Id: id.Value,
}
dlcSpecs = append(dlcSpecs, dlcSpec)
}
return &CrOSService{
Connection: common_utils.NewServiceAdapter(dutClient, req.GetProvisionState().GetPreventReboot()),
ImagePath: req.GetProvisionState().SystemImage.SystemImagePath,
OverwritePayload: req.GetProvisionState().GetSystemImage().GetOverwritePayload(),
PreserveStateful: false,
DlcSpecs: dlcSpecs,
MachineMetadata: metadata.MachineMetadata{},
UpdateFirmware: req.GetProvisionState().UpdateFirmware,
UpdateCros: true, // Force this true by default
}
}
// NewCrOSServiceFromExistingConnection is equivalent to the above constructor,
// but recycles a ServiceAdapter. Generally useful for tests.
func NewCrOSServiceFromExistingConnection(conn common_utils.ServiceAdapterInterface, imagePath *conf.StoragePath, overwritePayload *conf.StoragePath, preserverStateful bool, dlcSpecs []*api.CrOSProvisionMetadata_DLCSpec, updateFirmware bool) CrOSService {
return CrOSService{
Connection: conn,
ImagePath: imagePath,
OverwritePayload: overwritePayload,
PreserveStateful: preserverStateful,
DlcSpecs: dlcSpecs,
MachineMetadata: metadata.MachineMetadata{},
UpdateFirmware: updateFirmware,
UpdateCros: true, // Force this true by default
}
}
// CleanupOnFailure is called if one of service's states fails to Execute() and
// should clean up the temporary files, and undo the execution, if feasible.
func (c *CrOSService) CleanupOnFailure(states []common_utils.ServiceState, executionErr error) error {
// TODO: evaluate whether cleanup is needed.
return nil
}
const pipeStatusHandler = `
pipestatus=("${PIPESTATUS[@]}")
if [[ "${pipestatus[0]}" -ne 0 ]]; then
echo "$(date --rfc-3339=seconds) ERROR: Fetching %[1]s failed." >&2
exit 1
elif [[ "${pipestatus[1]}" -ne 0 ]]; then
echo "$(date --rfc-3339=seconds) ERROR: Decompressing %[1]s failed." >&2
exit 1
elif [[ "${pipestatus[2]}" -ne 0 ]]; then
echo "$(date --rfc-3339=seconds) ERROR: Writing to %[2]s failed." >&2
exit 1
fi`
// TODO(kimjae): Refactor InstallZippedImage/InstallZstdCompressedFile into a single entity such as InstallCompressedFile, with additional options to select the compression method.
// InstallZippedImage installs a remote zipped image to disk.
func (c *CrOSService) InstallZippedImage(ctx context.Context, remoteImagePath string, outputFile string) error {
if c.ImagePath.HostType == conf.StoragePath_LOCAL || c.ImagePath.HostType == conf.StoragePath_HOSTTYPE_UNSPECIFIED {
return fmt.Errorf("only GS copying is implemented")
}
err := c.Connection.PipeData(ctx,
common_utils.BucketJoin(c.ImagePath.GetPath(), remoteImagePath),
fmt.Sprintf("gzip -d | %s %s", fmt.Sprintf("dd of=%s obs=2M", outputFile), fmt.Sprintf(pipeStatusHandler, c.ImagePath.GetPath(), outputFile)),
)
if err != nil {
return fmt.Errorf("failed to install image, %w", err)
}
return nil
}
// InstallZstdCompressedFile installs a compressed (zstd) file to target.
func (c *CrOSService) InstallZstdCompressedFile(ctx context.Context, remoteImagePath string, outputPath string) error {
if c.ImagePath.HostType == conf.StoragePath_LOCAL || c.ImagePath.HostType == conf.StoragePath_HOSTTYPE_UNSPECIFIED {
return fmt.Errorf("only GS copying is implemented")
}
err := c.Connection.PipeData(ctx,
common_utils.BucketJoin(c.ImagePath.GetPath(), remoteImagePath),
// Fine to reuse the pipe status handler for now.
fmt.Sprintf("zstdcat | %s %s", fmt.Sprintf("dd of=%s obs=2M", outputPath), fmt.Sprintf(pipeStatusHandler, c.ImagePath.GetPath(), outputPath)),
)
if err != nil {
return fmt.Errorf("failed to install image, %w", err)
}
return nil
}
// unpackMetadata unpacks the Any metadata field into CrOSProvisionMetadata
func unpackMetadata(req *api.InstallRequest) (*api.CrOSProvisionMetadata, error) {
m := api.CrOSProvisionMetadata{}
if err := req.Metadata.UnmarshalTo(&m); err != nil {
return &m, fmt.Errorf("improperly formatted input proto metadata, %s", err)
}
return &m, nil
}