blob: 05185d37dc4d735c568a5c4cb89c31925a826b84 [file] [log] [blame]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package main implements the cros-provision used to setup CrOS devices.
package main
import (
"go.chromium.org/chromiumos/test/provision/cmd/provisionserver"
"context"
"flag"
"fmt"
"io"
"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"
"go.chromium.org/luci/common/errors"
)
const (
// version is the version info of this command. It is filled in during emerge.
version = "<unknown>"
helpDescription = `cros-provision tool
The tool is allow to perform provision ChromeOS devices.
Please read go/cros-provision-f20 for mode details.
Commands:
cli Running provision in CLI mode result will be printed to output file. Mostly used to prepare
device before running tests.
usage: cros-provision cli -input input_file -output output_file [-log-path /tmp/provision/]
server Starting server and allow work with server by RPC calls. Mostly used for tests.
usage: cros-provision server -input input_file [-log-path /tmp/provision/] [-port 80]
-version Print version of lib.
-help Print this help.`
defaultLogDirectory = "/tmp/provision/"
defaultPort = 80
)
// createLogFile creates a file and its parent directory for logging purpose.
func createLogFile(dir string) (*os.File, error) {
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("failed to create directory %v: %v", dir, err)
}
logFilePath := filepath.Join(dir, "log.txt")
// Log the full output of the command to disk.
logFile, err := os.Create(logFilePath)
if err != nil {
return nil, fmt.Errorf("failed to create file %v: %v", logFilePath, err)
}
return logFile, nil
}
// newLogger creates a logger. Using go default logger for now.
func newLogger(logFile *os.File) *log.Logger {
mw := io.MultiWriter(logFile, os.Stderr)
return log.New(mw, "", log.LstdFlags|log.LUTC)
}
type args struct {
// Common input params.
logPath string
inputPath string
outputPath string
// Server mode params
port int
}
func (a args) validate(expectProvisionState bool) (*api.CrosProvisionRequest, error) {
if a.inputPath == "" {
return nil, errors.Reason("input file not specified").Err()
}
if a.outputPath == "" {
return nil, errors.Reason("output file not specified").Err()
}
in, err := readInput(a.inputPath)
if err != nil {
return nil, errors.Annotate(err, "fail to read input file").Err()
}
if expectProvisionState && in.GetProvisionState() == nil {
return nil, errors.Reason("provision state was not specified in input file").Err()
}
if in.GetDut() == nil || in.GetDut().GetId().GetValue() == "" {
return nil, errors.Reason("dut id is not specified in input file").Err()
}
if isAddressEmpty(in.GetDutServer()) {
return nil, errors.Reason("dut server address is no specified or incorrect in input file").Err()
}
return in, nil
}
// readInput reads input request data from the input file.
func readInput(path string) (*api.CrosProvisionRequest, error) {
in := &api.CrosProvisionRequest{}
r, err := os.Open(path)
if err != nil {
return nil, errors.Annotate(err, "read input").Err()
}
umrsh := jsonpb.Unmarshaler{}
umrsh.AllowUnknownFields = true
err = umrsh.Unmarshal(r, in)
return in, errors.Annotate(err, "read input").Err()
}
func isAddressEmpty(a *lab_api.IpEndpoint) bool {
return a == nil || a.GetAddress() == "" || a.GetPort() <= 0
}
func getAddress(a *lab_api.IpEndpoint) string {
return fmt.Sprintf("%s:%d", a.GetAddress(), a.GetPort())
}
func runCLI(ctx context.Context, d []string) int {
a := args{}
fs := flag.NewFlagSet("Run provision", flag.ExitOnError)
fs.StringVar(&a.logPath, "log-path", defaultLogDirectory, fmt.Sprintf("Path to record execution logs. Default value is %s", defaultLogDirectory))
fs.StringVar(&a.inputPath, "input", "", "Specify the request jsonproto input file. Provide service paths and ProvisionState.")
fs.StringVar(&a.outputPath, "output", "", "Specify the response jsonproto output file. Empty placeholder file to provide result from provisioning the DUT.")
fs.Parse(d)
logFile, err := createLogFile(a.logPath)
if err != nil {
log.Fatalln("Failed to create log file", err)
return 2
}
defer logFile.Close()
logger := newLogger(logFile)
in, err := a.validate(true)
if err != nil {
log.Fatalf("Validate input fail: %s", err)
return 2
}
p, closer, err := provisionserver.NewProvision(logger, in.GetDut(), getAddress(in.GetDutServer()))
defer closer()
if err != nil {
logger.Fatalln("Failed to create provision: ", err)
return 2
}
if err := p.RunCLI(ctx, in.GetProvisionState(), a.outputPath); err != nil {
logger.Fatalln("Failed to perform provision: ", err)
return 1
}
return 0
}
func startServer(d []string) int {
a := args{}
fs := flag.NewFlagSet("Start provision server", flag.ExitOnError)
fs.StringVar(&a.logPath, "log-path", defaultLogDirectory, fmt.Sprintf("Path to record execution logs. Default value is %s", defaultLogDirectory))
fs.StringVar(&a.inputPath, "input", "", "Specify the request jsonproto input file. Provide service paths and ProvisionState.")
fs.IntVar(&a.port, "port", defaultPort, fmt.Sprintf("Specify the port for the server. Default value %d.", defaultPort))
fs.Parse(d)
logFile, err := createLogFile(a.logPath)
if err != nil {
log.Fatalln("Failed to create log file", err)
return 2
}
defer logFile.Close()
logger := newLogger(logFile)
in, err := a.validate(false)
if err != nil {
log.Fatalf("Validate input fail: %s", err)
return 2
}
p, closer, err := provisionserver.NewProvision(logger, in.GetDut(), getAddress(in.GetDutServer()))
defer closer()
if err != nil {
logger.Fatalln("Failed to create provision: ", err)
return 2
}
if err := p.StartServer(a.port); err != nil {
logger.Fatalln("Failed to perform provision: ", err)
return 1
}
return 0
}
// Specify run mode for CLI.
type runMode string
const (
runCli runMode = "cli"
runServer runMode = "server"
runVersion runMode = "version"
runHelp runMode = "help"
)
func getRunMode() (runMode, error) {
if len(os.Args) > 1 {
for _, a := range os.Args {
if a == "-version" {
return runVersion, nil
}
}
switch strings.ToLower(os.Args[1]) {
case "cli":
return runCli, nil
case "server":
return runServer, nil
}
}
// If we did not find special run mode then just print help for user.
return runHelp, nil
}
func mainInternal(ctx context.Context) int {
rm, err := getRunMode()
if err != nil {
log.Fatalln(err)
return 2
}
switch rm {
case runCli:
log.Printf("Running CLI mode!")
return runCLI(ctx, os.Args[2:])
case runServer:
log.Printf("Running server mode!")
return startServer(os.Args[2:])
case runVersion:
log.Printf("cros-provisions version: %s", version)
return 0
}
log.Printf(helpDescription)
return 0
}
func main() {
os.Exit(mainInternal(context.Background()))
}