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