blob: 606ab94ead9060ed7ba5ddf91f14cbe7db9eba73 [file] [log] [blame]
// Copyright 2020 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 dut
import (
"context"
"flag"
"fmt"
"os"
"strings"
"github.com/golang/protobuf/proto"
"github.com/maruel/subcommands"
"go.chromium.org/luci/auth/client/authcli"
"go.chromium.org/luci/common/cli"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/grpc/prpc"
"google.golang.org/genproto/protobuf/field_mask"
"infra/cmd/shivas/cmdhelp"
"infra/cmd/shivas/site"
"infra/cmd/shivas/utils"
suUtil "infra/cmd/shivas/utils/schedulingunit"
"infra/cmdsupport/cmdlib"
swarming "infra/libs/swarming"
ufspb "infra/unifiedfleet/api/v1/models"
chromeosLab "infra/unifiedfleet/api/v1/models/chromeos/lab"
ufsAPI "infra/unifiedfleet/api/v1/rpc"
ufsUtil "infra/unifiedfleet/app/util"
)
const (
// Servo related UpdateMask paths.
servoHostPath = "dut.servo.hostname"
servoPortPath = "dut.servo.port"
servoSerialPath = "dut.servo.serial"
servoSetupPath = "dut.servo.setup"
servoFwChannelPath = "dut.servo.fwchannel"
servoTypePath = "dut.servo.type"
servoTopologyPath = "dut.servo.topology"
// LSE related UpdateMask paths.
machinesPath = "machines"
descriptionPath = "description"
tagsPath = "tags"
ticketPath = "deploymentTicket"
// RPM related UpdateMask paths.
rpmHostPath = "dut.rpm.host"
rpmOutletPath = "dut.rpm.outlet"
// DUT related UpdateMask paths.
poolsPath = "dut.pools"
licensePath = "dut.licenses"
// ACS related UpdateMask paths.
chameleonsPath = "dut.chameleon.type"
chameleonsAudioBoardPath = "dut.chameleon.audioboard"
cameraTypePath = "dut.camera.type"
audioBoxPath = "dut.audio.box"
atrusPath = "dut.audio.atrus"
audioCablePath = "dut.audio.cable"
cablePath = "dut.cable.type"
wifiAntennaPath = "dut.wifi.antennaconn"
wifiCellPath = "dut.wifi.wificell"
wifiRouterPath = "dut.wifi.router"
touchMimoPath = "dut.touch.mimo"
carrierPath = "dut.carrier"
chaosPath = "dut.chaos"
cameraboxPath = "dut.camerabox"
cameraFacingPath = "dut.camerabox.facing"
cameraLightPath = "dut.camerabox.light"
usbHubPath = "dut.usb.smarthub"
// Operations string for Summary table.
ufsOp = "Update to Database"
swarmOp = "Deployment"
)
// partialUpdateDeployPaths is a collection of paths for which there is a partial update on servo/rpm.
var partialUpdateDeployPaths = []string{servoHostPath, servoPortPath, servoSerialPath, servoSetupPath, rpmHostPath, rpmOutletPath}
// partialUpdateDeployActions is a collection of actions for the deploy task when updating servo/rpm.
var partialUpdateDeployActions = []string{
"run-pre-deploy-verification",
}
// partialUpdateDeployActions is a collection of actions for the deploy task when updating machines.
var assetUpdateDeployActions = []string{
"servo-verification",
"stage-usb",
"install-test-image",
"install-firmware",
"verify-recovery-mode",
"update-label",
"run-pre-deploy-verification",
}
// UpdateDUTCmd update dut by given hostname and start a swarming job to delpoy.
var UpdateDUTCmd = &subcommands.Command{
UsageLine: "dut [options]",
ShortDesc: "Update a DUT",
LongDesc: cmdhelp.UpdateDUTLongDesc,
CommandRun: func() subcommands.CommandRun {
c := &updateDUT{
pools: []string{},
deployTags: shivasTags,
}
// Initialize servo setup types
c.authFlags.Register(&c.Flags, site.DefaultAuthOptions)
c.envFlags.Register(&c.Flags)
c.commonFlags.Register(&c.Flags)
c.Flags.StringVar(&c.newSpecsFile, "f", "", cmdhelp.DUTUpdateFileText)
c.Flags.StringVar(&c.hostname, "name", "", "hostname of the DUT.")
c.Flags.StringVar(&c.machine, "asset", "", "asset tag of the DUT.")
c.Flags.StringVar(&c.servo, "servo", "", "servo hostname and port as hostname:port. Clearing this field will delete the servo in DUT. "+cmdhelp.ClearFieldHelpText)
c.Flags.StringVar(&c.servoSerial, "servo-serial", "", "serial number for the servo.")
c.Flags.StringVar(&c.servoSetupType, "servo-setup", "", "servo setup type. Allowed values are "+cmdhelp.ServoSetupTypeAllowedValuesString()+".")
c.Flags.StringVar(&c.servoFwChannel, "servo-fw-channel", "", "servo firmware channel. Allowed values are "+cmdhelp.ServoFwChannelAllowedValuesString()+".")
c.Flags.Var(utils.CSVString(&c.pools), "pools", "comma seperated pools. These will be appended to existing pools. "+cmdhelp.ClearFieldHelpText)
c.Flags.Var(utils.CSVString(&c.licenseTypes), "licensetype", cmdhelp.LicenseTypeHelpText)
c.Flags.Var(utils.CSVString(&c.licenseIds), "licenseid", "the name of the license type. Can specify multiple comma separated values. "+cmdhelp.ClearFieldHelpText)
c.Flags.StringVar(&c.rpm, "rpm", "", "rpm assigned to the DUT. Clearing this field will delete rpm. "+cmdhelp.ClearFieldHelpText)
c.Flags.StringVar(&c.rpmOutlet, "rpm-outlet", "", "rpm outlet used for the DUT.")
c.Flags.StringVar(&c.deploymentTicket, "ticket", "", "the deployment ticket for this machine. "+cmdhelp.ClearFieldHelpText)
c.Flags.Var(utils.CSVString(&c.tags), "tags", "comma separated tags. You can only append new tags or delete all of them. "+cmdhelp.ClearFieldHelpText)
c.Flags.StringVar(&c.description, "desc", "", "description for the machine. "+cmdhelp.ClearFieldHelpText)
c.Flags.Int64Var(&c.deployTaskTimeout, "deploy-timeout", swarming.DeployTaskExecutionTimeout, "execution timeout for deploy task in seconds.")
c.Flags.BoolVar(&c.forceDeploy, "force-deploy", false, "forces a deploy task for all the updates.")
c.Flags.Var(utils.CSVString(&c.deployTags), "deploy-tags", "comma seperated tags for deployment task.")
c.Flags.BoolVar(&c.forceDownloadImage, "force-download-image", false, "force download image and stage usb if deploy task is run.")
c.Flags.BoolVar(&c.forceInstallFirmware, "force-install-fw", false, "force install firmware if deploy task is run.")
c.Flags.BoolVar(&c.forceInstallOS, "force-install-os", false, "force install os image if deploy task is run.")
c.Flags.BoolVar(&c.forceUpdateLabels, "force-update-labels", false, "force update labels if deploy task is run.")
// ACS DUT fields
c.Flags.Var(utils.CSVString(&c.chameleons), "chameleons", cmdhelp.ChameleonTypeHelpText+". "+cmdhelp.ClearFieldHelpText)
c.Flags.Var(utils.CSVString(&c.cameras), "cameras", cmdhelp.CameraTypeHelpText+". "+cmdhelp.ClearFieldHelpText)
c.Flags.Var(utils.CSVString(&c.cables), "cables", cmdhelp.CableTypeHelpText+". "+cmdhelp.ClearFieldHelpText)
c.Flags.StringVar(&c.antennaConnection, "antennaconnection", "", cmdhelp.AntennaConnectionHelpText)
c.Flags.StringVar(&c.router, "router", "", cmdhelp.RouterHelpText)
c.Flags.StringVar(&c.facing, "facing", "", cmdhelp.FacingHelpText)
c.Flags.StringVar(&c.light, "light", "", cmdhelp.LightHelpText)
c.Flags.StringVar(&c.carrier, "carrier", "", "name of the carrier."+". "+cmdhelp.ClearFieldHelpText)
c.Flags.BoolVar(&c.audioBoard, "audioboard", false, "adding this flag will specify if audioboard is present")
c.Flags.BoolVar(&c.audioBox, "audiobox", false, "adding this flag will specify if audiobox is present")
c.Flags.BoolVar(&c.atrus, "atrus", false, "adding this flag will specify if atrus is present")
c.Flags.BoolVar(&c.wifiCell, "wificell", false, "adding this flag will specify if wificell is present")
c.Flags.BoolVar(&c.touchMimo, "touchmimo", false, "adding this flag will specify if touchmimo is present")
c.Flags.BoolVar(&c.cameraBox, "camerabox", false, "adding this flag will specify if camerabox is present")
c.Flags.BoolVar(&c.chaos, "chaos", false, "adding this flag will specify if chaos is present")
c.Flags.BoolVar(&c.audioCable, "audiocable", false, "adding this flag will specify if audiocable is present")
c.Flags.BoolVar(&c.smartUSBHub, "smartusbhub", false, "adding this flag will specify if smartusbhub is present")
return c
},
}
type updateDUT struct {
subcommands.CommandRunBase
authFlags authcli.Flags
envFlags site.EnvFlags
commonFlags site.CommonFlags
// DUT specification inputs.
newSpecsFile string
hostname string
machine string
servo string
servoSerial string
servoSetupType string
servoFwChannel string
pools []string
licenseTypes []string
licenseIds []string
rpm string
rpmOutlet string
deploymentTicket string
tags []string
description string
// Deploy task inputs.
forceDeploy bool
deployTaskTimeout int64
deployTags []string
forceDownloadImage bool
forceInstallOS bool
forceInstallFirmware bool
forceUpdateLabels bool
// ACS DUT fields
chameleons []string
cameras []string
antennaConnection string
router string
cables []string
facing string
light string
carrier string
audioBoard bool
audioBox bool
atrus bool
wifiCell bool
touchMimo bool
cameraBox bool
chaos bool
audioCable bool
smartUSBHub bool
// For use in determining if a flag is set
flagInputs map[string]bool
}
func (c *updateDUT) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if err := c.innerRun(a, args, env); err != nil {
cmdlib.PrintError(a, err)
return 1
}
return 0
}
func (c *updateDUT) innerRun(a subcommands.Application, args []string, env subcommands.Env) error {
// Determine all the input flags and store them in the map.
c.flagInputs = make(map[string]bool)
c.Flags.Visit(func(f *flag.Flag) {
c.flagInputs[f.Name] = true
})
// Using a map to collect deploy actions. This ensures single deploy task per DUT.
var deployTasks map[string][]string
// Create a summary results table with 3 columns.
resTable := utils.NewSummaryResultsTable([]string{"DUT", ufsOp, swarmOp})
if err := c.validateArgs(); err != nil {
return err
}
ctx := cli.GetContext(a, c, env)
ctx = utils.SetupContext(ctx, ufsUtil.OSNamespace)
hc, err := cmdlib.NewHTTPClient(ctx, &c.authFlags)
if err != nil {
return err
}
e := c.envFlags.Env()
if c.commonFlags.Verbose() {
fmt.Printf("Using UFS service %s \n", e.UnifiedFleetService)
fmt.Printf("Using swarming service %s \n", e.SwarmingService)
}
requests, err := c.parseArgs()
if err != nil {
return err
}
// Create a map of DUTs to avoid triggering multiple tasks.
deployTasks = make(map[string][]string)
ic := ufsAPI.NewFleetPRPCClient(&prpc.Client{
C: hc,
Host: e.UnifiedFleetService,
Options: site.DefaultPRPCOptions,
})
for _, req := range requests {
// Collect the deploy actions required for the request. This is done before DUT is changed on UFS.
actions, err := c.getDeployActions(ctx, ic, req)
if err != nil {
return err
}
// Attempt to update UFS.
err = c.updateDUTToUFS(ctx, ic, req)
// Record the result of the action.
resTable.RecordResult(ufsOp, req.MachineLSE.GetName(), err)
if err != nil {
// Print err and skip deployment if it's not forced.
if !c.forceDeploy {
fmt.Printf("[%s] Error updating UFS. Skip triggering deploy task. %s\n", req.MachineLSE.GetName(), err.Error())
// Record the skip result.
resTable.RecordSkip(swarmOp, req.MachineLSE.GetName(), err.Error())
continue
}
fmt.Printf("[%s] Failed to update UFS. Attempting to trigger deploy task '-force-deploy'. %s\n", req.MachineLSE.GetName(), err.Error())
}
deployTasks[req.MachineLSE.GetName()] = actions
}
tc, err := swarming.NewTaskCreator(ctx, &c.authFlags, e.SwarmingService)
if err != nil {
return err
}
tc.LogdogService = e.LogdogService
tc.SwarmingServiceAccount = e.SwarmingServiceAccount
for _, req := range requests {
// Check if the deployment is needed.
actions, ok := deployTasks[req.MachineLSE.GetName()]
if !ok {
// Deploy Task not required.
continue
}
// Swarm a deploy task if required or enforced.
if len(actions) > 0 || c.forceDeploy {
// If deploy task is enforced and len(actions) = 0 and use partialUpdatedeployActions as default.
if len(actions) == 0 && c.forceDeploy {
actions = partialUpdateDeployActions
}
// Include any enforced actions.
actions = c.updateDeployActions(actions)
// Start a swarming deploy task for the DUT.
if err := c.deployDUTToSwarming(ctx, tc, req.GetMachineLSE(), actions); err != nil {
// Print err and continue to trigger next one
fmt.Printf("[%s] Failed to deploy task. %s", req.GetMachineLSE().GetName(), err.Error())
}
resTable.RecordResult(swarmOp, req.MachineLSE.GetName(), err)
// Remove the task entry to avoid triggering multiple tasks.
delete(deployTasks, req.MachineLSE.GetName())
}
}
if resTable.IsSuccessForAny(swarmOp) {
// Display URL for all tasks if there are more than one.
fmt.Printf("\nTriggered deployment task(s). Follow at: %s\n", tc.SessionTasksURL())
}
fmt.Printf("\nSummary of results:\n\n")
resTable.PrintResultsTable(os.Stdout, true)
return nil
}
// validateArgs validates the set of inputs to updateDUT.
func (c updateDUT) validateArgs() error {
if c.newSpecsFile == "" && c.hostname == "" {
return cmdlib.NewQuietUsageError(c.Flags, "Need hostname to create a DUT")
}
if c.newSpecsFile == "" {
// Check if servo input is valid
if c.servo != "" && c.servo != utils.ClearFieldValue {
_, _, err := parseServoHostnamePort(c.servo)
if err != nil {
return err
}
}
// Check if servo type is valid.
// Note: This check is run irrespective of servo input because it is possible to perform an update on only this field.
if _, ok := chromeosLab.ServoSetupType_value[appendServoSetupPrefix(c.servoSetupType)]; c.servoSetupType != "" && !ok {
return cmdlib.NewQuietUsageError(c.Flags, "Invalid value for servo setup type. Valid values are "+cmdhelp.ServoSetupTypeAllowedValuesString())
}
// Check if servo firmware channel is valid.
// Note: This check is run irrespective of servo input because it is possible to perform an update on only this field.
if _, ok := chromeosLab.ServoFwChannel_value[appendServoFwChannelPrefix(c.servoFwChannel)]; c.servoFwChannel != "" && !ok {
return cmdlib.NewQuietUsageError(c.Flags, "Invalid value for servo firmware channel. Valid values are "+cmdhelp.ServoFwChannelAllowedValuesString())
}
// Check if the license input is valid if it's not being cleared.
if !ufsUtil.ContainsAnyStrings(c.licenseIds, utils.ClearFieldValue) {
if len(c.licenseTypes) != len(c.licenseIds) {
return cmdlib.NewQuietUsageError(c.Flags, "Wrong usage!!\nNumber of -licensetype(%s) and -licenseid(%s) must be same.", c.licenseTypes, c.licenseIds)
}
for _, cp := range c.licenseTypes {
if !ufsUtil.IsLicenseType(cp) {
return cmdlib.NewQuietUsageError(c.Flags, "Wrong usage!!\n%s is not a valid license type name, please check help info for '-licensetype'.", cp)
}
}
}
for _, cp := range c.chameleons {
if !ufsUtil.IsChameleonType(cp) && cp != utils.ClearFieldValue {
return cmdlib.NewQuietUsageError(c.Flags, "Wrong usage!!\n%s is not a valid chameleon type name, please check help info for '-chameleons'.", cp)
}
}
for _, cp := range c.cameras {
if !ufsUtil.IsCameraType(cp) && cp != utils.ClearFieldValue {
return cmdlib.NewQuietUsageError(c.Flags, "Wrong usage!!\n%s is not a valid camera type name, please check help info for '-cameras'.", cp)
}
}
for _, cp := range c.cables {
if !ufsUtil.IsCableType(cp) && cp != utils.ClearFieldValue {
return cmdlib.NewQuietUsageError(c.Flags, "Wrong usage!!\n%s is not a valid cable type name, please check help info for '-cables'.", cp)
}
}
if c.antennaConnection != "" && !ufsUtil.IsAntennaConnection(c.antennaConnection) {
return cmdlib.NewQuietUsageError(c.Flags, "Wrong usage!!\n%s is not a valid antenna connection name, please check help info for '-antennaconnection'.", c.antennaConnection)
}
if c.router != "" && !ufsUtil.IsRouter(c.router) {
return cmdlib.NewQuietUsageError(c.Flags, "Wrong usage!!\n%s is not a valid router name, please check help info for '-router'.", c.router)
}
if c.facing != "" && !ufsUtil.IsFacing(c.facing) {
return cmdlib.NewQuietUsageError(c.Flags, "Wrong usage!!\n%s is not a valid facing name, please check help info for '-facing'.", c.facing)
}
if c.light != "" && !ufsUtil.IsLight(c.light) {
return cmdlib.NewQuietUsageError(c.Flags, "Wrong usage!!\n%s is not a valid light name, please check help info for '-light'.", c.light)
}
}
if c.newSpecsFile != "" {
// Helper function to return the formatted error.
f := func(input string) error {
return cmdlib.NewQuietUsageError(c.Flags, fmt.Sprintf("Wrong usage!!\nThe MCSV/JSON mode is specified. '-%s' cannot be specified at the same time.", input))
}
// Cannot accept cmdline inputs for DUT when csv/json mode is specified
// The following flags can be set with JSON/MCSV mode.
allowList := map[string]interface{}{
"dev": nil,
"f": nil,
"ticket": nil,
"tags": nil,
"desc": nil,
"deploy_timeout": nil,
"force-deploy": nil,
"deploy-tags": nil,
"force-download-image": nil,
"force-install-fw": nil,
"force-install-os": nil,
"force-update-labels": nil,
}
// If a flag not in allow list is set. Throw an error
for name, set := range c.flagInputs {
if set {
if _, ok := allowList[name]; !ok {
return f(name)
}
}
}
}
return nil
}
// validateRequest checks if the req is valid based on the cmdline input.
func (c *updateDUT) validateRequest(ctx context.Context, ic ufsAPI.FleetClient, req *ufsAPI.UpdateMachineLSERequest) error {
lse := req.MachineLSE
mask := req.UpdateMask
if mask == nil || len(mask.Paths) == 0 {
if lse == nil {
return fmt.Errorf("Internal Error. Invalid UpdateMachineLSERequest")
}
if lse.Name == "" {
return fmt.Errorf("Invalid update. Missing DUT name")
}
}
return suUtil.CheckIfLSEBelongsToSU(ctx, ic, lse.GetName())
}
// parseArgs reads input from the cmd line parameters and generates update dut request.
func (c *updateDUT) parseArgs() ([]*ufsAPI.UpdateMachineLSERequest, error) {
if c.newSpecsFile != "" {
if utils.IsCSVFile(c.newSpecsFile) {
return c.parseMCSV()
}
machineLse := &ufspb.MachineLSE{}
if err := utils.ParseJSONFile(c.newSpecsFile, machineLse); err != nil {
return nil, err
}
if err := c.validateDUTFromJSON(machineLse); err != nil {
return nil, err
}
// json input updates without a mask.
return []*ufsAPI.UpdateMachineLSERequest{{
MachineLSE: machineLse,
}}, nil
}
lse, mask, err := c.initializeLSEAndMask(nil)
if err != nil {
return nil, err
}
return []*ufsAPI.UpdateMachineLSERequest{{
MachineLSE: lse,
UpdateMask: mask,
}}, nil
}
// validateDUTFromJSON checks if the input lse represents DUT and ensures servo/rpm isn't incomplete.
func (c *updateDUT) validateDUTFromJSON(dutLse *ufspb.MachineLSE) error {
if err := utils.IsDUT(dutLse); err != nil {
return errors.Annotate(err, "The LSE in %s is not a DUT", c.newSpecsFile).Err()
}
if servo := dutLse.GetChromeosMachineLse().GetDeviceLse().GetDut().GetPeripherals().GetServo(); servo != nil {
// Avoid incomplete servo updates.
if !(servo.GetServoHostname() != "" && servo.GetServoSerial() != "") {
// Note: ServoPort == int32(0) auto-assigns the port.
return cmdlib.NewQuietUsageError(c.Flags, "Incomplete/Invalid servo update in %s", c.newSpecsFile)
}
// Don't allow updates to ServoType or ServoTopology from here, unless its to clear them both by setting servoType to ClearFieldValue.
if servo.GetServoType() != "" && servo.GetServoType() != utils.ClearFieldValue {
return cmdlib.NewQuietUsageError(c.Flags, "Cannot set servo_type to %s in %s. Setting it to '%s' will update both servoType and servoTopology with correct values", servo.GetServoType(), c.newSpecsFile, utils.ClearFieldValue)
}
// Don't allow updates to servoTopology
if servo.GetServoTopology() != nil {
return cmdlib.NewQuietUsageError(c.Flags, "Cannot update ServoTopology using %s. Invalid usage", c.newSpecsFile)
}
}
if rpm := dutLse.GetChromeosMachineLse().GetDeviceLse().GetDut().GetPeripherals().GetRpm(); rpm != nil {
if (rpm.GetPowerunitName() != "" && rpm.GetPowerunitOutlet() == "") || (rpm.GetPowerunitName() == "" && rpm.GetPowerunitOutlet() != "") {
return cmdlib.NewQuietUsageError(c.Flags, "Cannot update incomplete RPM. Need both host and outlet")
}
}
return nil
}
// parseMCSV generates update request from mcsv file.
func (c *updateDUT) parseMCSV() ([]*ufsAPI.UpdateMachineLSERequest, error) {
records, err := utils.ParseMCSVFile(c.newSpecsFile)
if err != nil {
return nil, err
}
var requests []*ufsAPI.UpdateMachineLSERequest
for i, rec := range records {
if i == 0 && utils.LooksLikeHeader(rec) {
if err := utils.ValidateSameStringArray(mcsvFields, rec); err != nil {
return nil, err
}
continue
}
recMap := make(map[string]string)
for j, title := range mcsvFields {
recMap[title] = rec[j]
}
lse, mask, err := c.initializeLSEAndMask(recMap)
if err != nil {
// Print the error and the line number and continue to next one.
fmt.Printf("Error [%s:%v]: %s\n", c.newSpecsFile, i+1, err.Error())
continue
}
requests = append(requests, &ufsAPI.UpdateMachineLSERequest{
MachineLSE: lse,
UpdateMask: mask,
})
}
return requests, nil
}
func (c *updateDUT) initializeLSEAndMask(recMap map[string]string) (*ufspb.MachineLSE, *field_mask.FieldMask, error) {
var name, servo, servoSerial, servoSetup, rpmHost, rpmOutlet string
var pools, machines []string
if recMap != nil {
// CSV map. Assign all the params to the variables.
name = recMap["name"]
// Generate cmdline servo input. This allows for easier validation and assignment.
if recMap["servo_host"] != "" || recMap["servo_port"] != "" {
servo = fmt.Sprintf("%s:%s", recMap["servo_host"], recMap["servo_port"])
}
servoSerial = recMap["servo_serial"]
if recMap["servo_setup"] != "" {
servoSetup = appendServoSetupPrefix(recMap["servo_setup"])
}
rpmHost = recMap["rpm_host"]
rpmOutlet = recMap["rpm_outlet"]
machines = []string{recMap["asset"]}
pools = strings.Fields(recMap["pools"])
} else {
// command line parameters. Update vars with the correct values.
name = c.hostname
servo = c.servo
servoSerial = c.servoSerial
if c.servoSetupType != "" {
servoSetup = appendServoSetupPrefix(c.servoSetupType)
}
rpmHost = c.rpm
rpmOutlet = c.rpmOutlet
machines = []string{c.machine}
pools = c.pools
}
// Generate lse and mask
lse := &ufspb.MachineLSE{
Lse: &ufspb.MachineLSE_ChromeosMachineLse{
ChromeosMachineLse: &ufspb.ChromeOSMachineLSE{
ChromeosLse: &ufspb.ChromeOSMachineLSE_DeviceLse{
DeviceLse: &ufspb.ChromeOSDeviceLSE{
Device: &ufspb.ChromeOSDeviceLSE_Dut{
Dut: &chromeosLab.DeviceUnderTest{
Peripherals: &chromeosLab.Peripherals{
Chameleon: &chromeosLab.Chameleon{},
Servo: &chromeosLab.Servo{},
Rpm: &chromeosLab.OSRPM{},
Audio: &chromeosLab.Audio{},
Wifi: &chromeosLab.Wifi{},
Touch: &chromeosLab.Touch{},
CameraboxInfo: &chromeosLab.Camerabox{},
},
},
},
},
},
},
},
}
mask := &field_mask.FieldMask{}
lse.Name = name
lse.Hostname = name
lse.GetChromeosMachineLse().GetDeviceLse().GetDut().Hostname = name
peripherals := lse.GetChromeosMachineLse().GetDeviceLse().GetDut().GetPeripherals()
// Check if machines are being updated.
if len(machines) > 0 && machines[0] != "" {
lse.Machines = machines
mask.Paths = append(mask.Paths, machinesPath)
}
// Check and update pools if required.
if len(pools) > 0 && pools[0] != "" {
mask.Paths = append(mask.Paths, poolsPath)
// Check if user is clearing the pool
if ufsUtil.ContainsAnyStrings(pools, utils.ClearFieldValue) {
lse.GetChromeosMachineLse().GetDeviceLse().GetDut().Pools = nil
} else {
lse.GetChromeosMachineLse().GetDeviceLse().GetDut().Pools = pools
}
}
// Check and update licenses if required.
if c.flagInputs["licenseid"] {
mask.Paths = append(mask.Paths, licensePath)
if ufsUtil.ContainsAnyStrings(c.licenseIds, utils.ClearFieldValue) {
// Clear all the licenses.
lse.GetChromeosMachineLse().GetDeviceLse().GetDut().Licenses = nil
} else {
licenses := make([]*chromeosLab.License, 0, len(c.licenseTypes))
for i := range c.licenseTypes {
licenses = append(licenses, &chromeosLab.License{
Type: ufsUtil.ToLicenseType(c.licenseTypes[i]),
Identifier: c.licenseIds[i],
})
}
lse.GetChromeosMachineLse().GetDeviceLse().GetDut().Licenses = licenses
}
}
// Create and assign servo and corresponding masks.
newServo, paths, err := generateServoWithMask(servo, servoSetup, servoSerial, c.servoFwChannel)
if err != nil {
return nil, nil, err
}
peripherals.Servo = newServo
mask.Paths = append(mask.Paths, paths...)
// Create and assign rpm and corresponding masks.
rpm, paths := generateRPMWithMask(rpmHost, rpmOutlet)
peripherals.Rpm = rpm
mask.Paths = append(mask.Paths, paths...)
// Check if description field is being updated/cleared.
if c.description != "" {
mask.Paths = append(mask.Paths, descriptionPath)
if c.description != utils.ClearFieldValue {
lse.Description = c.description
} else {
lse.Description = ""
}
}
// Check if deployment ticket is being updated/cleared.
if c.deploymentTicket != "" {
mask.Paths = append(mask.Paths, ticketPath)
if c.deploymentTicket != utils.ClearFieldValue {
lse.DeploymentTicket = c.deploymentTicket
} else {
lse.DeploymentTicket = ""
}
}
// Check if tags are being appended/deleted. Tags can either be appended or cleared.
if len(c.tags) > 0 {
mask.Paths = append(mask.Paths, tagsPath)
lse.Tags = c.tags
// Check if utils.ClearFieldValue is included in any of the tags.
if ufsUtil.ContainsAnyStrings(c.tags, utils.ClearFieldValue) {
lse.Tags = nil
}
}
// ACS DUT fields
// Chameleon Type
if c.flagInputs["chameleons"] && len(c.chameleons) > 0 {
mask.Paths = append(mask.Paths, chameleonsPath)
// Check if utils.ClearFieldValue is included in any of the chameleon inputs.
if ufsUtil.ContainsAnyStrings(c.chameleons, utils.ClearFieldValue) {
// Clearing all the chameleons.
peripherals.GetChameleon().ChameleonPeripherals = nil
} else {
chameleons := make([]chromeosLab.ChameleonType, 0, len(c.chameleons))
for _, cp := range c.chameleons {
chameleons = append(chameleons, ufsUtil.ToChameleonType(cp))
}
peripherals.GetChameleon().ChameleonPeripherals = chameleons
}
}
// Cameras
if c.flagInputs["cameras"] && len(c.cameras) > 0 {
mask.Paths = append(mask.Paths, cameraTypePath)
// Check if utils.ClearFieldValue is included in any of the camera inputs.
if ufsUtil.ContainsAnyStrings(c.cameras, utils.ClearFieldValue) {
// Clearing all the cameras.
peripherals.ConnectedCamera = nil
} else {
cameras := make([]*chromeosLab.Camera, 0, len(c.cameras))
for _, cp := range c.cameras {
camera := &chromeosLab.Camera{
CameraType: ufsUtil.ToCameraType(cp),
}
cameras = append(cameras, camera)
}
peripherals.ConnectedCamera = cameras
}
}
// Cables
if c.flagInputs["cables"] && len(c.cables) > 0 {
mask.Paths = append(mask.Paths, cablePath)
// Check if utils.ClearFieldValue is included in any of the cable inputs.
if ufsUtil.ContainsAnyStrings(c.cables, utils.ClearFieldValue) {
// Clearing all the cables.
peripherals.Cable = nil
} else {
cables := make([]*chromeosLab.Cable, 0, len(c.cables))
for _, cp := range c.cables {
cable := &chromeosLab.Cable{
Type: ufsUtil.ToCableType(cp),
}
cables = append(cables, cable)
}
peripherals.Cable = cables
}
}
// AntennaConn
if c.flagInputs["antennaconnection"] {
mask.Paths = append(mask.Paths, wifiAntennaPath)
peripherals.GetWifi().AntennaConn = ufsUtil.ToAntennaConnection(c.antennaConnection)
}
// Router
if c.flagInputs["router"] && c.router != "" {
mask.Paths = append(mask.Paths, wifiRouterPath)
peripherals.GetWifi().Router = ufsUtil.ToRouter(c.router)
}
// Camerabox Facing
if c.flagInputs["facing"] && c.facing != "" {
mask.Paths = append(mask.Paths, cameraFacingPath)
peripherals.GetCameraboxInfo().Facing = ufsUtil.ToFacing(c.facing)
}
//Camerabox Light
if c.flagInputs["light"] && c.light != "" {
mask.Paths = append(mask.Paths, cameraLightPath)
peripherals.GetCameraboxInfo().Light = ufsUtil.ToLight(c.light)
}
// AudioBoard
if c.flagInputs["audioboard"] {
mask.Paths = append(mask.Paths, chameleonsAudioBoardPath)
peripherals.GetChameleon().AudioBoard = c.audioBoard
}
// AudioBox
if c.flagInputs["audiobox"] {
mask.Paths = append(mask.Paths, audioBoxPath)
peripherals.GetAudio().AudioBox = c.audioBox
}
// Atrus
if c.flagInputs["atrus"] {
mask.Paths = append(mask.Paths, atrusPath)
peripherals.GetAudio().Atrus = c.atrus
}
// AudioCable
if c.flagInputs["audiocable"] {
mask.Paths = append(mask.Paths, audioCablePath)
peripherals.GetAudio().AudioCable = c.audioCable
}
// WifiCell
if c.flagInputs["wificell"] {
mask.Paths = append(mask.Paths, wifiCellPath)
peripherals.GetWifi().Wificell = c.wifiCell
}
// TouchMimo
if c.flagInputs["touchmimo"] {
mask.Paths = append(mask.Paths, touchMimoPath)
peripherals.GetTouch().Mimo = c.touchMimo
}
// Carrier
if c.flagInputs["carrier"] {
if c.carrier == utils.ClearFieldValue {
// Clear the carrier if required
c.carrier = ""
}
mask.Paths = append(mask.Paths, carrierPath)
peripherals.Carrier = c.carrier
}
// CameraBox
if c.flagInputs["camerabox"] {
mask.Paths = append(mask.Paths, cameraboxPath)
peripherals.Camerabox = c.cameraBox
}
// Chaos
if c.flagInputs["chaos"] {
mask.Paths = append(mask.Paths, chaosPath)
peripherals.Chaos = c.chaos
}
// SmartUSBHub
if c.flagInputs["smartusbhub"] {
mask.Paths = append(mask.Paths, usbHubPath)
peripherals.SmartUsbhub = c.smartUSBHub
}
// Check if nothing is being updated. Updating with an empty mask overwrites everything.
if !c.forceDeploy && (len(mask.Paths) == 0 || mask.Paths[0] == "") {
return nil, nil, cmdlib.NewQuietUsageError(c.Flags, "Nothing to update")
}
return lse, mask, nil
}
// generateServoWithMask generates a servo object from the given inputs and corresponding mask.
func generateServoWithMask(servo, servoSetup, servoSerial, servoFwChannel string) (*chromeosLab.Servo, []string, error) {
if servo == "" && servoSetup == "" && servoSerial == "" && servoFwChannel == "" {
return nil, nil, nil
}
// Attempt to parse servo hostname and port.
servoHost, servoPort, err := parseServoHostnamePort(servo)
if err != nil {
return nil, nil, err
}
// If servo is being deleted. Return nil with mask path for servo. Ignore other params.
if servoHost == utils.ClearFieldValue {
return nil, []string{servoHostPath}, nil
}
newServo := &chromeosLab.Servo{}
var paths []string
// Check and update servo port.
if servoPort != int32(0) {
paths = append(paths, servoPortPath)
newServo.ServoPort = servoPort
}
if servoSetup != "" {
paths = append(paths, servoSetupPath)
sst := chromeosLab.ServoSetupType(chromeosLab.ServoSetupType_value[appendServoSetupPrefix(servoSetup)])
newServo.ServoSetup = sst
}
if servoFwChannel != "" {
paths = append(paths, servoFwChannelPath)
sst := chromeosLab.ServoFwChannel(chromeosLab.ServoFwChannel_value[appendServoFwChannelPrefix(servoFwChannel)])
newServo.ServoFwChannel = sst
}
if servoSerial != "" {
paths = append(paths, servoSerialPath)
newServo.ServoSerial = servoSerial
}
if servoHost != "" {
paths = append(paths, servoHostPath)
newServo.ServoHostname = servoHost
}
if servoHost != "" || servoSerial != "" || servoSetup != "" || servoPort != int32(0) {
// Clear servo_type and servo_topology before deploying. Specifying path only assigns default empty values.
paths = append(paths, servoTypePath, servoTopologyPath)
}
return newServo, paths, nil
}
// generateRPMWithMask generates a rpm object from the given inputs and corresponding mask.
func generateRPMWithMask(rpmHost, rpmOutlet string) (*chromeosLab.OSRPM, []string) {
// Check if rpm is being deleted.
if rpmHost == utils.ClearFieldValue {
// Generate mask and empty rpm.
return nil, []string{rpmHostPath}
}
rpm := &chromeosLab.OSRPM{}
paths := []string{}
// Check and update rpm.
if rpmHost != "" {
rpm.PowerunitName = rpmHost
paths = append(paths, rpmHostPath)
}
if rpmOutlet != "" {
rpm.PowerunitOutlet = rpmOutlet
paths = append(paths, rpmOutletPath)
}
return rpm, paths
}
// updateDeployActions updates the deploySkipActions based on boolean force options.
func (c *updateDUT) updateDeployActions(actions []string) []string {
// Append the enforced deploy actions.
if c.forceDownloadImage && !ufsUtil.ContainsAnyStrings(actions, "stage-usb") {
actions = append(actions, "stage-usb")
}
if c.forceInstallOS && !ufsUtil.ContainsAnyStrings(actions, "install-test-image") {
actions = append(actions, "install-test-image")
}
if c.forceInstallFirmware {
if !ufsUtil.ContainsAnyStrings(actions, "install-firmware") {
actions = append(actions, "install-firmware")
}
if !ufsUtil.ContainsAnyStrings(actions, "verify-recovery-mode") {
actions = append(actions, "verify-recovery-mode")
}
}
if (c.forceInstallFirmware || c.forceInstallOS || c.forceUpdateLabels) && !ufsUtil.ContainsAnyStrings(actions, "update-label") {
actions = append(actions, "update-label")
}
return actions
}
// getDeployActions checks the machineLse request and decides actions required for the deploy task.
//
// Actions for deploy task are determined based on the following.
// 1. Updates to servo/rpm will start deploy task with run-pre-deploy-verification.
// 2. Updates to asset will start deploy task with stage-usb, install-test-image, install-firmware,
// update-label, verify-recovery-mode and run-pre-deploy-verification
// 3. If both are updated then asset takes precedence and actions in (2) are run.
// 4. If neither of them is found. Return nil, nil.
func (c *updateDUT) getDeployActions(ctx context.Context, ic ufsAPI.FleetClient, req *ufsAPI.UpdateMachineLSERequest) (a []string, err error) {
defer func() {
// Cannot trust JSON input to have all the fields. Log error.
if r := recover(); r != nil {
if c.newSpecsFile != "" && !utils.IsCSVFile(c.newSpecsFile) {
// JSON update might be missing some fields.
err = errors.Reason("getDeployActions - Error: %v. Check %s for errors.", r, c.newSpecsFile).Err()
} else {
// InternalError. This should not happen.
err = errors.Reason("getDeployActions - InternalError: %v.", r).Err()
}
a = nil
return
}
}()
// Check if its partial update. Determine actions and state based on what's being updated.
if req.UpdateMask != nil && len(req.UpdateMask.Paths) > 0 {
if ufsUtil.ContainsAnyStrings(req.UpdateMask.Paths, "machines") {
// Asset update. Set state to manual_repair.
req.MachineLSE.ResourceState = ufspb.State_STATE_DEPLOYED_TESTING
return assetUpdateDeployActions, nil
}
if ufsUtil.ContainsAnyStrings(req.UpdateMask.Paths, partialUpdateDeployPaths...) {
// RPM/Servo update set state to manual_repair.
req.MachineLSE.ResourceState = ufspb.State_STATE_DEPLOYED_TESTING
// Append any options that were set to force and return.
return partialUpdateDeployActions, nil
}
return nil, nil
}
// Check if it's a JSON update and validate full update.
if c.newSpecsFile != "" && !utils.IsCSVFile(c.newSpecsFile) {
// Full update requires verifying what's being changed on the existing DUT.
newDut := req.MachineLSE
// Get the existing DUT configuration.
oldDut, err := ic.GetMachineLSE(ctx, &ufsAPI.GetMachineLSERequest{
Name: ufsUtil.AddPrefix(ufsUtil.MachineLSECollection, newDut.GetName()),
})
// If DUT doesn't exist return error as update will fail.
if err != nil {
return nil, errors.Annotate(err, "getDeployActions - Please check if DUT exists before updating. Failed to get DUT %s", newDut.GetName()).Err()
}
// Fail if the target is not a DUT.
if err := utils.IsDUT(oldDut); err != nil {
return nil, errors.Annotate(err, "getDeployActions - %s is not a DUT", oldDut.GetName()).Err()
}
// Check if asset was updated.
if oldDut.GetMachines()[0] != newDut.GetMachines()[0] {
// Asset update. Set state to manual_repair.
req.MachineLSE.ResourceState = ufspb.State_STATE_DEPLOYED_TESTING
return assetUpdateDeployActions, nil
}
// Check for any servo changes. Need to run a deploy task for the following cases
// 1. Reset/Delete servo. [newServo == nil || newServo.ServoHostname = ""]
// 2. Adding a new servo. [oldServo == nil || oldServo.ServoHostname = ""]
// 3. Clear servo type. [newServo.ServoType == ClearFieldValue]
// 4. Update servo. [newServo != nil && oldServo != nil]
var oldServo, newServo *chromeosLab.Servo
// Check if we are deleting servo.
newServo = req.MachineLSE.GetChromeosMachineLse().GetDeviceLse().GetDut().GetPeripherals().GetServo()
if newServo == nil || newServo.GetServoHostname() == "" {
// Ensure delete.
req.MachineLSE.GetChromeosMachineLse().GetDeviceLse().GetDut().GetPeripherals().Servo = nil
// Servo update set state to manual_repair.
req.MachineLSE.ResourceState = ufspb.State_STATE_DEPLOYED_TESTING
return partialUpdateDeployActions, nil
}
// Check if the user intends to clear servo type and topology
if newServo.GetServoType() == utils.ClearFieldValue {
// Clear servo_type and servo_topology as it will be updated by deploy task
newServo.ServoType = ""
newServo.ServoTopology = nil
// Servo update set state to manual_repair.
req.MachineLSE.ResourceState = ufspb.State_STATE_DEPLOYED_TESTING
// Need to run deploy task.
return partialUpdateDeployActions, nil
}
// Check if we are adding a new servo.
oldServo = oldDut.GetChromeosMachineLse().GetDeviceLse().GetDut().GetPeripherals().GetServo()
if oldServo == nil || oldServo.GetServoHostname() == "" {
// Servo update set state to manual_repair.
req.MachineLSE.ResourceState = ufspb.State_STATE_DEPLOYED_TESTING
return partialUpdateDeployActions, nil
}
// Check if servo was updated by the user.
// Make a copy of oldServo for comparison.
oldServoCopy := proto.Clone(oldServo).(*chromeosLab.Servo)
// Don't compare servo type or topology as it's not input by the user.
oldServoCopy.ServoType = ""
oldServoCopy.ServoTopology = nil
// Check if the servo host/port/serial is updated.
if !ufsUtil.ProtoEqual(oldServoCopy, newServo) {
// Servo update set state to manual_repair.
req.MachineLSE.ResourceState = ufspb.State_STATE_DEPLOYED_TESTING
return partialUpdateDeployActions, nil
}
// User doesn't intend to update servo. Avoid calling the deploy task and copy servo_type and topology from oldServo.
newServo.ServoType = oldServo.GetServoType()
newServo.ServoTopology = oldServo.GetServoTopology()
req.MachineLSE.GetChromeosMachineLse().GetDeviceLse().GetDut().GetPeripherals().Servo = newServo
// Check if rpm was updated.
var oldRpm, newRpm *chromeosLab.OSRPM
// Get existing rpm from the DUT.
if p := oldDut.GetChromeosMachineLse().GetDeviceLse().GetDut().GetPeripherals(); p != nil {
oldRpm = p.GetRpm()
}
newRpm = req.MachineLSE.GetChromeosMachineLse().GetDeviceLse().GetDut().GetPeripherals().GetRpm()
// Check if anything in RPM was updated.
if !ufsUtil.ProtoEqual(oldRpm, newRpm) {
// RPM update set state to manual_repair.
req.MachineLSE.ResourceState = ufspb.State_STATE_DEPLOYED_TESTING
// Append any options that were set to force and return.
return partialUpdateDeployActions, nil
}
}
// Didn't find any reason to run deploy task.
return nil, nil
}
// updateDUTToUFS verifies the request and calls UpdateMachineLSE API with the given request.
func (c *updateDUT) updateDUTToUFS(ctx context.Context, ic ufsAPI.FleetClient, req *ufsAPI.UpdateMachineLSERequest) error {
// Validate the update request.
if err := c.validateRequest(ctx, ic, req); err != nil {
return err
}
// Print existing LSE before update.
if err := utils.PrintExistingDUT(ctx, ic, req.MachineLSE.GetName()); err != nil {
return err
}
req.MachineLSE.Name = ufsUtil.AddPrefix(ufsUtil.MachineLSECollection, req.MachineLSE.Name)
res, err := ic.UpdateMachineLSE(ctx, req)
if err != nil {
return err
}
// Remove prefix from the request. It's used for comparison later.
req.MachineLSE.Name = ufsUtil.RemovePrefix(req.MachineLSE.Name)
res.Name = ufsUtil.RemovePrefix(res.Name)
utils.PrintProtoJSON(res, !utils.NoEmitMode(false))
fmt.Printf("Successfully updated DUT to UFS: %s \n", res.GetName())
return nil
}
// deployDUTToSwarming starts a re-deploy task for the given DUT.
func (c *updateDUT) deployDUTToSwarming(ctx context.Context, tc *swarming.TaskCreator, lse *ufspb.MachineLSE, actions []string) error {
var hostname, machine string
// Using hostname because name has resource prefix
hostname = lse.GetHostname()
machines := lse.GetMachines()
if len(machines) > 0 {
machine = machines[0]
}
task, err := tc.DeployDut(ctx, hostname, machine, defaultSwarmingPool, c.deployTaskTimeout, actions, c.deployTags, nil)
if err != nil {
return err
}
fmt.Printf("Triggered Deploy task for DUT %s. Follow the deploy job at %s\n", hostname, task.TaskURL)
return nil
}
func appendServoFwChannelPrefix(servoFwChannel string) string {
return fmt.Sprintf("SERVO_FW_%s", servoFwChannel)
}