blob: 85d37b3a6f52c29bd66afc71ccae447ca4aeda9b [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.
package cli
import (
"context"
"errors"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/golang/protobuf/jsonpb"
"go.chromium.org/chromiumos/config/go/test/api"
lab_api "go.chromium.org/chromiumos/config/go/test/lab/api"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"go.chromium.org/chromiumos/test/provision/v2/android-provision/common"
"go.chromium.org/chromiumos/test/provision/v2/android-provision/service"
state_machine "go.chromium.org/chromiumos/test/provision/v2/android-provision/state-machine"
"go.chromium.org/chromiumos/test/provision/v2/android-provision/test"
common_utils "go.chromium.org/chromiumos/test/provision/v2/common-utils"
)
var statusToResult = map[api.InstallResponse_Status]api.InstallFailure_Reason{
api.InstallResponse_STATUS_INVALID_REQUEST: api.InstallFailure_REASON_INVALID_REQUEST,
api.InstallResponse_STATUS_DUT_UNREACHABLE_PRE_PROVISION: api.InstallFailure_REASON_DUT_UNREACHABLE_PRE_PROVISION,
api.InstallResponse_STATUS_DOWNLOADING_IMAGE_FAILED: api.InstallFailure_REASON_DOWNLOADING_IMAGE_FAILED,
api.InstallResponse_STATUS_PROVISIONING_FAILED: api.InstallFailure_REASON_PROVISIONING_FAILED,
api.InstallResponse_STATUS_POST_PROVISION_SETUP_FAILED: api.InstallFailure_REASON_POST_PROVISION_SETUP_FAILED,
api.InstallResponse_STATUS_PRE_PROVISION_SETUP_FAILED: api.InstallFailure_REASON_PRE_PROVISION_SETUP_FAILED,
api.InstallResponse_STATUS_CIPD_PACKAGE_LOOKUP_FAILED: api.InstallFailure_REASON_CIPD_PACKAGE_LOOKUP_FAILED,
api.InstallResponse_STATUS_CIPD_PACKAGE_FETCH_FAILED: api.InstallFailure_REASON_CIPD_PACKAGE_FETCH_FAILED,
api.InstallResponse_STATUS_GS_UPLOAD_FAILED: api.InstallFailure_REASON_GS_UPLOAD_FAILED,
api.InstallResponse_STATUS_GS_DOWNLOAD_FAILED: api.InstallFailure_REASON_GS_DOWNLOAD_FAILED,
}
// CLICommand executed the provisioning as a CLI
type CLICommand struct {
logFileName string
log *log.Logger
inputFile string
inputProto *api.AndroidProvisionRequest
outputFile string
flagSet *flag.FlagSet
}
func NewCLICommand() *CLICommand {
cc := &CLICommand{
flagSet: flag.NewFlagSet("cli", flag.ContinueOnError),
}
cc.flagSet.StringVar(&cc.logFileName, "log-path", common.DefaultLogDirectory, fmt.Sprintf("Path to record execution logs. Default value is %s", common.DefaultLogDirectory))
cc.flagSet.StringVar(&cc.inputFile, "input", "", "Specify the request jsonproto input file. Provide service paths and ProvisionState.")
cc.flagSet.StringVar(&cc.outputFile, "output", "", "Specify the response jsonproto output file. Empty placeholder file to provide result from provisioning the DUT.")
return cc
}
func (cc *CLICommand) Is(group string) bool {
return strings.HasPrefix(group, "c")
}
func (cc *CLICommand) Name() string {
return "cli"
}
func (cc *CLICommand) Init(args []string) error {
err := cc.flagSet.Parse(args)
if err != nil {
return err
}
cc.log, err = common.SetUpLog(cc.logFileName)
if err != nil {
return err
}
if err = cc.validate(); err != nil {
return err
}
cc.inputProto, err = common_utils.ParseAndroidProvisionRequest(cc.inputFile)
if err != nil {
return fmt.Errorf("unable to parse AndroidProvisionRequest proto: %s", err)
}
return nil
}
func (cc *CLICommand) Usage() {
cc.flagSet.Usage()
}
// Logger returns the log
func (cc *CLICommand) Logger() *log.Logger {
return cc.log
}
// validate checks if inputs are ok
func (cc *CLICommand) validate() error {
if cc.inputFile == "" {
return errors.New("input file not specified")
}
if cc.outputFile == "" {
return errors.New("output file not specified")
}
return nil
}
func (cc *CLICommand) Run() error {
cc.log.Printf("Running CLI Mode (V2):")
dutAddr := fmt.Sprintf("%s:%d", cc.inputProto.GetDutServer().GetAddress(), cc.inputProto.GetDutServer().GetPort())
cc.log.Printf("DutAddr: %s", dutAddr)
dutConn, err := grpc.Dial(dutAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return fmt.Errorf("failed to connect to dut-service: %s", err)
}
cc.log.Printf("Dut Conn Established")
defer dutConn.Close()
ip := cc.inputProto.GetDutServer().GetAddress()
var svc *service.AndroidService
// Use local adapter for testing. See testing folder.
if ip == "127.0.0.1" {
svcAdapter, err := test.NewLocalDutServiceAdapter(cc.inputProto.GetDutServer())
if err != nil {
return fmt.Errorf("failed to create AndroidService: %s", err)
}
svc, err = service.NewAndroidServiceFromExistingConnection(svcAdapter, cc.inputProto.GetDut().GetAndroid().GetSerialNumber(), cc.inputProto.GetProvisionState().GetAndroidOsImage(), cc.inputProto.GetProvisionState().GetCipdPackages())
if err != nil {
return fmt.Errorf("failed to create Android service: %s", err)
}
} else {
svc, err = service.NewAndroidServiceFromAndroidProvisionRequest(api.NewDutServiceClient(dutConn), cc.inputProto)
}
cc.log.Printf("New AndroidService Created")
out := &api.AndroidProvisionCLIResponse{
Id: &lab_api.Dut_Id{
Value: cc.inputProto.GetDut().GetId().GetValue(),
},
Outcome: &api.AndroidProvisionCLIResponse_Success{},
}
defer cc.saveResponse(out)
cc.log.Printf("Starting State Machine.")
respStatus, _, err := common_utils.ExecuteStateMachine(context.Background(), state_machine.NewPrepareState(svc), cc.log)
if os := svc.OS; os != nil && os.UpdatedBuildInfo != nil {
out.InstalledAndroidOs = &api.InstalledAndroidOS{
BuildId: os.UpdatedBuildInfo.Id,
OsVersion: os.UpdatedBuildInfo.OsVersion,
IncrementalVersion: os.UpdatedBuildInfo.IncrementalVersion,
}
}
for _, pkg := range svc.ProvisionPackages {
if androidPkg := pkg.AndroidPackage; androidPkg != nil && androidPkg.UpdatedVersionCode != "" {
installedPkg := &api.InstalledAndroidPackage{
Name: androidPkg.PackageName,
VersionCode: androidPkg.UpdatedVersionCode,
}
out.AndroidPackages = append(out.AndroidPackages, installedPkg)
}
}
if err != nil {
cc.log.Printf("State Machine Failed: %v", err)
translatedStatus := statusToResult[respStatus]
out.Outcome = &api.AndroidProvisionCLIResponse_Failure{
Failure: &api.InstallFailure{
Reason: api.InstallFailure_Reason(translatedStatus),
},
}
return fmt.Errorf("failed to provision, %s", err)
}
return nil
}
// saveResponse saves response to the output file.
func (cc *CLICommand) saveResponse(out *api.AndroidProvisionCLIResponse) error {
cc.log.Printf("saveCLIOutput out:%s\n", out)
if cc.outputFile != "" && out != nil {
dir := filepath.Dir(cc.outputFile)
// Create the directory if it doesn't exist.
if err := os.MkdirAll(dir, 0777); err != nil {
return fmt.Errorf("save output: fail to create directory for %q", cc.outputFile)
}
w, err := os.Create(cc.outputFile)
if err != nil {
return fmt.Errorf("save output: failed to create file %q", cc.outputFile)
}
defer w.Close()
m := jsonpb.Marshaler{}
if err := m.Marshal(w, out); err != nil {
return fmt.Errorf("save output: failed to marshal output")
}
}
return nil
}