3c4b560c97a17a26f0b406eac5c769ff575e5b11
[arvados.git] / lib / cloud / cloudtest / cmd.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package cloudtest
6
7 import (
8         "bufio"
9         "errors"
10         "flag"
11         "fmt"
12         "io"
13         "os"
14
15         "git.arvados.org/arvados.git/lib/cloud"
16         "git.arvados.org/arvados.git/lib/config"
17         "git.arvados.org/arvados.git/lib/dispatchcloud"
18         "git.arvados.org/arvados.git/sdk/go/arvados"
19         "git.arvados.org/arvados.git/sdk/go/ctxlog"
20         "golang.org/x/crypto/ssh"
21 )
22
23 var Command command
24
25 type command struct{}
26
27 func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
28         var err error
29         defer func() {
30                 if err != nil {
31                         fmt.Fprintf(stderr, "%s\n", err)
32                 }
33         }()
34
35         flags := flag.NewFlagSet("", flag.ContinueOnError)
36         flags.SetOutput(stderr)
37         configFile := flags.String("config", arvados.DefaultConfigFile, "Site configuration `file`")
38         instanceSetID := flags.String("instance-set-id", "zzzzz-zzzzz-zzzzzzcloudtest", "InstanceSetID tag `value` to use on the test instance")
39         imageID := flags.String("image-id", "", "Image ID to use when creating the test instance (if empty, use cluster config)")
40         instanceType := flags.String("instance-type", "", "Instance type to create (if empty, use cheapest type in config)")
41         destroyExisting := flags.Bool("destroy-existing", false, "Destroy any existing instances tagged with our InstanceSetID, instead of erroring out")
42         shellCommand := flags.String("command", "", "Run an interactive shell command on the test instance when it boots")
43         pauseBeforeDestroy := flags.Bool("pause-before-destroy", false, "Prompt and wait before destroying the test instance")
44         err = flags.Parse(args)
45         if err == flag.ErrHelp {
46                 err = nil
47                 return 0
48         } else if err != nil {
49                 return 2
50         } else if flags.NArg() != 0 {
51                 err = fmt.Errorf("unrecognized command line arguments: %v", flags.Args())
52                 return 2
53         }
54         logger := ctxlog.New(stderr, "text", "info")
55         defer func() {
56                 if err != nil {
57                         logger.WithError(err).Error("fatal")
58                         // suppress output from the other error-printing func
59                         err = nil
60                 }
61                 logger.Info("exiting")
62         }()
63
64         loader := config.NewLoader(stdin, logger)
65         loader.Path = *configFile
66         cfg, err := loader.Load()
67         if err != nil {
68                 return 1
69         }
70         cluster, err := cfg.GetCluster("")
71         if err != nil {
72                 return 1
73         }
74         key, err := ssh.ParsePrivateKey([]byte(cluster.Containers.DispatchPrivateKey))
75         if err != nil {
76                 err = fmt.Errorf("error parsing configured Containers.DispatchPrivateKey: %s", err)
77                 return 1
78         }
79         driver, ok := dispatchcloud.Drivers[cluster.Containers.CloudVMs.Driver]
80         if !ok {
81                 err = fmt.Errorf("unsupported cloud driver %q", cluster.Containers.CloudVMs.Driver)
82                 return 1
83         }
84         if *imageID == "" {
85                 *imageID = cluster.Containers.CloudVMs.ImageID
86         }
87         it, err := chooseInstanceType(cluster, *instanceType)
88         if err != nil {
89                 return 1
90         }
91         tags := cloud.SharedResourceTags(cluster.Containers.CloudVMs.ResourceTags)
92         tagKeyPrefix := cluster.Containers.CloudVMs.TagKeyPrefix
93         tags[tagKeyPrefix+"CloudTestPID"] = fmt.Sprintf("%d", os.Getpid())
94         if !(&tester{
95                 Logger:           logger,
96                 Tags:             tags,
97                 TagKeyPrefix:     tagKeyPrefix,
98                 SetID:            cloud.InstanceSetID(*instanceSetID),
99                 DestroyExisting:  *destroyExisting,
100                 ProbeInterval:    cluster.Containers.CloudVMs.ProbeInterval.Duration(),
101                 SyncInterval:     cluster.Containers.CloudVMs.SyncInterval.Duration(),
102                 TimeoutBooting:   cluster.Containers.CloudVMs.TimeoutBooting.Duration(),
103                 Driver:           driver,
104                 DriverParameters: cluster.Containers.CloudVMs.DriverParameters,
105                 ImageID:          cloud.ImageID(*imageID),
106                 InstanceType:     it,
107                 SSHKey:           key,
108                 SSHPort:          cluster.Containers.CloudVMs.SSHPort,
109                 BootProbeCommand: cluster.Containers.CloudVMs.BootProbeCommand,
110                 ShellCommand:     *shellCommand,
111                 PauseBeforeDestroy: func() {
112                         if *pauseBeforeDestroy {
113                                 logger.Info("waiting for operator to press Enter")
114                                 fmt.Fprint(stderr, "Press Enter to continue: ")
115                                 bufio.NewReader(stdin).ReadString('\n')
116                         }
117                 },
118         }).Run() {
119                 return 1
120         }
121         return 0
122 }
123
124 // Return the named instance type, or the cheapest type if name=="".
125 func chooseInstanceType(cluster *arvados.Cluster, name string) (arvados.InstanceType, error) {
126         if len(cluster.InstanceTypes) == 0 {
127                 return arvados.InstanceType{}, errors.New("no instance types are configured")
128         } else if name == "" {
129                 first := true
130                 var best arvados.InstanceType
131                 for _, it := range cluster.InstanceTypes {
132                         if first || best.Price > it.Price {
133                                 best = it
134                                 first = false
135                         }
136                 }
137                 return best, nil
138         } else if it, ok := cluster.InstanceTypes[name]; !ok {
139                 return it, fmt.Errorf("requested instance type %q is not configured", name)
140         } else {
141                 return it, nil
142         }
143 }