blob: 089dcc945ae626e56a0d95742bc052e25d4f29fd [file] [log] [blame]
// Copyright 2019 The Chromium 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 cmd
import (
"context"
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/maruel/subcommands"
"go.chromium.org/luci/auth/client/authcli"
swarming "go.chromium.org/luci/common/api/swarming/swarming/v1"
"go.chromium.org/luci/common/cli"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/flag"
"infra/cmd/skylab/internal/site"
)
// CreateSuite subcommand: create a suite task.
var CreateSuite = &subcommands.Command{
UsageLine: "create-suite [FLAGS...] SUITE_NAME",
ShortDesc: "create a suite task",
LongDesc: `Create a suite task, with the given suite name.
You must supply -pool and -image.
This command does not wait for the task to start running.`,
CommandRun: func() subcommands.CommandRun {
c := &createSuiteRun{}
c.authFlags.Register(&c.Flags, site.DefaultAuthOptions)
c.envFlags.Register(&c.Flags)
c.Flags.StringVar(&c.board, "board", "", "Board to run suite on.")
c.Flags.StringVar(&c.model, "model", "", "Model to run suite on.")
c.Flags.StringVar(&c.pool, "pool", "", "Device pool to run suite on.")
c.Flags.StringVar(&c.image, "image", "", "Fully specified image name to run suite against, e.g. reef-canary/R73-11580.0.0")
c.Flags.IntVar(&c.priority, "priority", defaultTaskPriority,
`Specify the priority of the suite. A high value means this suite
will be executed in a low priority.`)
c.Flags.IntVar(&c.timeoutMins, "timeout-mins", 20,
`Time (counting from when the task starts) after which task will be
killed if it hasn't completed.`)
c.Flags.IntVar(&c.maxRetries, "max-retries", 0,
`Maximum retries allowed in total for all child tests of this
suite. No retry if it is 0.`)
c.Flags.Var(flag.StringSlice(&c.tags), "tag", "Swarming tag for suite; may be specified multiple times.")
c.Flags.Var(flag.StringSlice(&c.keyvals), "keyval",
`Autotest keyval for test. Key may not contain : character. May be
specified multiple times.`)
c.Flags.StringVar(&c.qsAccount, "qs-account", "", "Quotascheduler account for test jobs.")
c.Flags.BoolVar(&c.orphan, "orphan", false, "Create a suite that doesn't wait for its child tests to finish. Internal or expert use ONLY!")
c.Flags.BoolVar(&c.json, "json", false, "Format output as JSON")
return c
},
}
type createSuiteRun struct {
subcommands.CommandRunBase
authFlags authcli.Flags
envFlags envFlags
board string
model string
pool string
image string
priority int
timeoutMins int
maxRetries int
tags []string
keyvals []string
qsAccount string
orphan bool
json bool
}
func (c *createSuiteRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
if err := c.innerRun(a, args, env); err != nil {
PrintError(a.GetErr(), err)
return 1
}
return 0
}
func (c *createSuiteRun) innerRun(a subcommands.Application, args []string, env subcommands.Env) error {
if err := c.validateArgs(); err != nil {
return err
}
ctx := cli.GetContext(a, c, env)
e := c.envFlags.Env()
suiteName := c.Flags.Arg(0)
dimensions := []string{"pool:ChromeOSSkylab-suite"}
keyvals, err := toKeyvalMap(c.keyvals)
if err != nil {
return err
}
slices, err := getSuiteSlices(c.board, c.model, c.pool, c.image, suiteName, c.qsAccount, c.priority, c.timeoutMins, c.maxRetries, dimensions, keyvals, c.orphan)
if err != nil {
return errors.Annotate(err, "create suite").Err()
}
tags := append(c.tags,
"skylab-tool:create-suite",
"luci_project:"+e.LUCIProject,
"build:"+c.image,
"suite:"+suiteName,
"label-board:"+c.board,
"label-model:"+c.model,
"label-pool:"+c.pool,
"priority:"+strconv.Itoa(c.priority))
s, err := newSwarmingService(ctx, c.authFlags, e)
if err != nil {
return errors.Annotate(err, "failed to create Swarming client").Err()
}
task := taskInfo{
Name: c.image + "-" + suiteName,
}
task.ID, err = createSuiteTask(ctx, s, task.Name, c.priority, slices, tags)
if err != nil {
return errors.Annotate(err, "create suite").Err()
}
task.URL = swarmingTaskURL(e, task.ID)
if c.json {
return json.NewEncoder(a.GetOut()).Encode(task)
}
fmt.Fprintf(a.GetOut(), "Created Swarming Suite task %s\n", task.URL)
return nil
}
func (c *createSuiteRun) validateArgs() error {
if c.Flags.NArg() == 0 {
return NewUsageError(c.Flags, "missing suite name")
}
if c.board == "" {
return NewUsageError(c.Flags, "missing -board")
}
if c.pool == "" {
return NewUsageError(c.Flags, "missing -pool")
}
if c.image == "" {
return NewUsageError(c.Flags, "missing -image")
}
if c.priority < 50 || c.priority > 255 {
return NewUsageError(c.Flags, "priority should in [50,255]")
}
return nil
}
func newTaskSlice(command []string, dimensions []*swarming.SwarmingRpcsStringPair, timeoutMins int) *swarming.SwarmingRpcsTaskSlice {
return &swarming.SwarmingRpcsTaskSlice{
ExpirationSecs: 300,
WaitForCapacity: false,
Properties: &swarming.SwarmingRpcsTaskProperties{
Command: command,
Dimensions: dimensions,
ExecutionTimeoutSecs: int64(timeoutMins * 60),
},
}
}
func getSuiteSlices(board string, model string, pool string, image string, suiteName string, qsAccount string, priority int, timeoutMins int, maxRetries int, dimensions []string, keyvals map[string]string, orphan bool) ([]*swarming.SwarmingRpcsTaskSlice, error) {
dims, err := toPairs(dimensions)
if err != nil {
return nil, errors.Annotate(err, "create slices").Err()
}
cmd := getRunSuiteCmd(board, model, pool, image, suiteName, qsAccount, priority, timeoutMins, maxRetries, keyvals, orphan)
return []*swarming.SwarmingRpcsTaskSlice{newTaskSlice(cmd, dims, timeoutMins)}, nil
}
func getRunSuiteCmd(board string, model string, pool string, image string, suiteName string, qsAccount string, priority int, timeoutMins int, maxRetries int, keyvals map[string]string, orphan bool) []string {
cmd := []string{
"/usr/local/autotest/bin/run_suite_skylab",
"--build", image,
"--board", board,
"--pool", pool,
"--suite_name", suiteName,
"--priority", strconv.Itoa(priority),
"--timeout_mins", strconv.Itoa(timeoutMins)}
if orphan {
cmd = append(cmd, "--create_and_return")
}
if model != "" {
cmd = append(cmd, "--model", model)
}
if qsAccount != "" {
cmd = append(cmd, "--quota_account", qsAccount)
}
if maxRetries > 0 {
cmd = append(cmd, "--test_retry")
cmd = append(cmd, "--max_retries", strconv.Itoa(maxRetries))
}
if len(keyvals) > 0 {
keyvalsJSON, err := json.Marshal(keyvals)
if err != nil {
panic(err)
}
cmd = append(cmd, "--job_keyvals", string(keyvalsJSON))
}
return cmd
}
func createSuiteTask(ctx context.Context, s *swarming.Service, taskName string, priority int, slices []*swarming.SwarmingRpcsTaskSlice, tags []string) (taskID string, err error) {
req := &swarming.SwarmingRpcsNewTaskRequest{
Name: taskName,
Tags: tags,
TaskSlices: slices,
Priority: int64(priority),
}
ctx, cf := context.WithTimeout(ctx, 60*time.Second)
defer cf()
resp, err := swarmingCreateTaskWithRetries(ctx, s, req)
if err != nil {
return "", errors.Annotate(err, "create suite").Err()
}
return resp.TaskId, nil
}