blob: f85ee74600a8af6b91f68855ba681eac00104fdf [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.
// LaCrOSInstall state machine construction and helper
package lacrosservice
import (
"chromiumos/test/provision/cmd/provisionserver/bootstrap/info"
"chromiumos/test/provision/cmd/provisionserver/bootstrap/services"
"context"
"encoding/json"
"fmt"
"log"
"path"
"strconv"
"strings"
conf "go.chromium.org/chromiumos/config/go"
"go.chromium.org/chromiumos/config/go/test/api"
"google.golang.org/grpc"
)
// LaCrOSService inherits ServiceInterface
type LaCrOSService struct {
connection services.ServiceAdapterInterface
imagePath *conf.StoragePath
metadata *LaCrOSMetadata
}
func NewLaCrOSService(dutName string, dutClient api.DutServiceClient, wiringConn *grpc.ClientConn, req *api.InstallLacrosRequest) (LaCrOSService, error) {
service := LaCrOSService{
connection: services.NewServiceAdapter(dutName, dutClient, wiringConn, false /*noReboot*/),
imagePath: req.LacrosImagePath,
}
metadata, err := service.ExtractLacrosMetadata(context.Background())
if err != nil {
return service, fmt.Errorf("could not extract metadata, %w", err)
}
service.metadata = metadata
return service, nil
}
// NewLaCrOSServiceFromExistingConnection is equivalent to the above constructor,
// but recycles a ServiceAdapter. Generally useful for tests.
func NewLaCrOSServiceFromExistingConnection(conn services.ServiceAdapterInterface, imagePath *conf.StoragePath, metadata *LaCrOSMetadata) LaCrOSService {
return LaCrOSService{
connection: conn,
imagePath: imagePath,
metadata: metadata,
}
}
// GetFirstState returns the first state of this state machine
func (c *LaCrOSService) GetFirstState() services.ServiceState {
return LaCrOSInstallState{
service: *c,
}
}
/*
The following consists of helper structs
*/
type LaCrOSMetadata struct {
Content struct {
Version string `json:"version"`
} `json:"content"`
}
/*
The following run specific commands related to LaCrOS installation.
*/
func (l *LaCrOSService) GetMetatadaPath() string {
return path.Join(l.imagePath.GetPath(), "metadata.json")
}
func (l *LaCrOSService) GetCompressedImagePath() string {
return path.Join(l.imagePath.GetPath(), "lacros_compressed.squash")
}
func (l *LaCrOSService) GetComponentPath() string {
return path.Join(info.LaCrOSRootComponentPath, l.metadata.Content.Version)
}
func (l *LaCrOSService) GetLocalImagePath() string {
return path.Join(l.GetComponentPath(), "image.squash")
}
func (l *LaCrOSService) GetHashTreePath() string {
return path.Join(l.GetComponentPath(), "hashtree")
}
func (l *LaCrOSService) GetTablePath() string {
return path.Join(l.GetComponentPath(), "table")
}
func (l *LaCrOSService) GetManifestPath() string {
return path.Join(l.GetComponentPath(), "imageloader.json")
}
func (l *LaCrOSService) GetComponentManifestPath() string {
return path.Join(l.GetComponentPath(), "manifest.json")
}
func (l *LaCrOSService) GetLatestVersionPath() string {
return path.Join(info.LaCrOSRootComponentPath, "latest-version")
}
// extractLacrosMetadata will unmarshal the metadata.json in the GS path into the state.
func (l *LaCrOSService) ExtractLacrosMetadata(ctx context.Context) (*LaCrOSMetadata, error) {
if l.imagePath.HostType == conf.StoragePath_LOCAL || l.imagePath.HostType == conf.StoragePath_HOSTTYPE_UNSPECIFIED {
return nil, fmt.Errorf("only GS copying is implemented")
}
url, err := l.connection.CopyData(ctx, l.GetMetatadaPath())
if err != nil {
return nil, fmt.Errorf("failed to cache Lacros metadata.json, %w", err)
}
metadataJSONStr, err := l.connection.RunCmd(ctx, "curl", []string{"-s", url})
if err != nil {
return nil, fmt.Errorf("failed to read Lacros metadata.json, %w", err)
}
metadataJSON := LaCrOSMetadata{}
if err := json.Unmarshal([]byte(metadataJSONStr), &metadataJSON); err != nil {
return nil, fmt.Errorf("failed to unmarshal Lacros metadata.json, %w", err)
}
return &metadataJSON, nil
}
// CopyImageToDUT copies the desired image to the DUT, passing through the caching layer.
func (l *LaCrOSService) CopyImageToDUT(ctx context.Context) error {
if l.imagePath.HostType == conf.StoragePath_LOCAL || l.imagePath.HostType == conf.StoragePath_HOSTTYPE_UNSPECIFIED {
return fmt.Errorf("only GS copying is implemented")
}
url, err := l.connection.CopyData(ctx, l.GetCompressedImagePath())
if err != nil {
return fmt.Errorf("failed to cache lacross compressed, %w", err)
}
if _, err := l.connection.RunCmd(ctx, "", []string{
"mkdir", "-p", l.GetComponentPath(),
"&&",
"curl", url, "--output", l.GetLocalImagePath(),
}); err != nil {
return fmt.Errorf("failed to copy lacross compressed, %w", err)
}
return nil
}
// RunVerity generates the verity (hashtree and table) from Lacros image.
func (l *LaCrOSService) RunVerity(ctx context.Context, payloadBlocks int) error {
// Generate the verity (hashtree and table) from Lacros image.
if _, err := l.connection.RunCmd(ctx, "verity", []string{
"mode=create",
"alg=sha256",
fmt.Sprintf("payload=%s", l.GetLocalImagePath()),
fmt.Sprintf("payload_blocks=%d", payloadBlocks),
fmt.Sprintf("hashtree=%s", l.GetHashTreePath()),
"salt=random",
">",
l.GetTablePath(),
}); err != nil {
return fmt.Errorf("failed to generate verity for Lacros image, %w", err)
}
return nil
}
// Append the hashtree (merkle tree) onto the end of the Lacros image.
func (l *LaCrOSService) AppendHashtree(ctx context.Context) error {
if _, err := l.connection.RunCmd(ctx, "cat", []string{
l.GetHashTreePath(),
">>",
l.GetLocalImagePath(),
}); err != nil {
return fmt.Errorf("failed to append hashtree to Lacros image, %w", err)
}
return nil
}
// AlignImageToPage will align the file to LacrosPageSize page alignment and return the number of page blocks.
func (l *LaCrOSService) AlignImageToPage(ctx context.Context) (int, error) {
sizeStr, err := l.connection.RunCmd(ctx, "stat", []string{"-c%s", l.GetLocalImagePath()})
if err != nil {
return 0, fmt.Errorf("failed to get image size, %w", err)
}
size, err := strconv.Atoi(strings.TrimSpace(sizeStr))
if err != nil {
return 0, fmt.Errorf("failed to get image size as an integer, %w", err)
}
// Round up to the nearest LaCrOSPageSize block size.
blocks := (size + info.LaCrOSPageSize - 1) / info.LaCrOSPageSize
// Check if the Lacros image is LacrosPageSize aligned, if not extend it to LacrosPageSize alignment.
if size != blocks*info.LaCrOSPageSize {
log.Printf("image %s isn't aligned to %d, so extending it", l.GetLocalImagePath(), info.LaCrOSPageSize)
inputBlockCount := blocks*info.LaCrOSPageSize - size
if _, err := l.connection.RunCmd(
ctx,
"dd",
[]string{"if=/dev/zero",
"bs=1",
fmt.Sprintf("count=%d", inputBlockCount),
fmt.Sprintf("seek=%d", size),
fmt.Sprintf("of=%s", l.GetLocalImagePath()),
}); err != nil {
return 0, fmt.Errorf("failed to align image to %d, %w", info.LaCrOSPageSize, err)
}
}
return blocks, nil
}
// getSHA256Sum will get the SHA256 sum of a file on the device.
func (l *LaCrOSService) getSHA256Sum(ctx context.Context, path string) (string, error) {
hash, err := l.connection.RunCmd(ctx, "sha256sum", []string{
path,
"|",
"cut", "-d' '", "-f1",
})
if err != nil {
return "", fmt.Errorf("failed to get hash of %s, %w", path, err)
}
return strings.TrimSpace(hash), nil
}
// writeToFile takes a string and writes its contents to a file on a DUT.
func (l *LaCrOSService) writeToFile(ctx context.Context, data, outputPath string) error {
if _, err := l.connection.RunCmd(ctx, "echo", []string{
fmt.Sprintf("'%s'", data), ">", outputPath,
}); err != nil {
return fmt.Errorf("failed to write data to file, %w", err)
}
return nil
}
// writeManifest will create and write the Lacros component manifest out.
func (l *LaCrOSService) writeManifest(ctx context.Context) error {
imageHash, err := l.getSHA256Sum(ctx, l.GetLocalImagePath())
if err != nil {
return fmt.Errorf("failed to get Lacros image hash, %w", err)
}
tableHash, err := l.getSHA256Sum(ctx, l.GetTablePath())
if err != nil {
return fmt.Errorf("failed to get Lacros table hash, %w", err)
}
lacrosManifestJSON, err := json.MarshalIndent(struct {
ManifestVersion int `json:"manifest-version"`
FsType string `json:"fs-type"`
Version string `json:"version"`
ImageSha256Hash string `json:"image-sha256-hash"`
TableSha256Hash string `json:"table-sha256-hash"`
}{
ManifestVersion: 1,
FsType: "squashfs",
Version: l.metadata.Content.Version,
ImageSha256Hash: imageHash,
TableSha256Hash: tableHash,
}, "", " ")
if err != nil {
return fmt.Errorf("failed to Marshal Lacros manifest json, %w", err)
}
return l.writeToFile(ctx, string(lacrosManifestJSON), l.GetManifestPath())
}
// writeComponentManifest will create and write the Lacros component manifest out usable by component updater.
func (l *LaCrOSService) writeComponentManifest(ctx context.Context) error {
lacrosComponentManifestJSON, err := json.MarshalIndent(struct {
ManifestVersion int `json:"manifest-version"`
Name string `json:"name"`
Version string `json:"version"`
ImageName string `json:"imageName"`
Squash bool `json:"squash"`
FsType string `json:"fsType"`
IsRemovable bool `json:"isRemovable"`
}{
ManifestVersion: 2,
Name: "lacros",
Version: l.metadata.Content.Version,
ImageName: "image.squash",
Squash: true,
FsType: "squashfs",
IsRemovable: false,
}, "", " ")
if err != nil {
return fmt.Errorf("writeComponentManifest: failed to Marshal Lacros manifest json, %w", err)
}
return l.writeToFile(ctx, string(lacrosComponentManifestJSON), l.GetComponentManifestPath())
}
// PublishVersion writes the Lacros version to the latest-version file.
func (l *LaCrOSService) PublishVersion(ctx context.Context) error {
return l.writeToFile(ctx, l.metadata.Content.Version, l.GetLatestVersionPath())
}