| // 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()) |
| } |