1package commands 2 3import ( 4 "errors" 5 "fmt" 6 "os" 7 "strings" 8 9 "github.com/codegangsta/cli" 10 "github.com/docker/machine/commands/mcndirs" 11 "github.com/docker/machine/libmachine" 12 "github.com/docker/machine/libmachine/crashreport" 13 "github.com/docker/machine/libmachine/host" 14 "github.com/docker/machine/libmachine/log" 15 "github.com/docker/machine/libmachine/mcnerror" 16 "github.com/docker/machine/libmachine/mcnutils" 17 "github.com/docker/machine/libmachine/persist" 18 "github.com/docker/machine/libmachine/ssh" 19) 20 21const ( 22 defaultMachineName = "default" 23) 24 25var ( 26 ErrHostLoad = errors.New("All specified hosts had errors loading their configuration") 27 ErrNoDefault = fmt.Errorf("Error: No machine name(s) specified and no %q machine exists", defaultMachineName) 28 ErrNoMachineSpecified = errors.New("Error: Expected to get one or more machine names as arguments") 29 ErrExpectedOneMachine = errors.New("Error: Expected one machine name as an argument") 30 ErrTooManyArguments = errors.New("Error: Too many arguments given") 31 32 osExit = func(code int) { os.Exit(code) } 33) 34 35// CommandLine contains all the information passed to the commands on the command line. 36type CommandLine interface { 37 ShowHelp() 38 39 ShowVersion() 40 41 Application() *cli.App 42 43 Args() cli.Args 44 45 IsSet(name string) bool 46 47 Bool(name string) bool 48 49 Int(name string) int 50 51 String(name string) string 52 53 StringSlice(name string) []string 54 55 GlobalString(name string) string 56 57 FlagNames() (names []string) 58 59 Generic(name string) interface{} 60} 61 62type contextCommandLine struct { 63 *cli.Context 64} 65 66func (c *contextCommandLine) ShowHelp() { 67 cli.ShowCommandHelp(c.Context, c.Command.Name) 68} 69 70func (c *contextCommandLine) ShowVersion() { 71 cli.ShowVersion(c.Context) 72} 73 74func (c *contextCommandLine) Application() *cli.App { 75 return c.App 76} 77 78// targetHost returns a specific host name if one is indicated by the first CLI 79// arg, or the default host name if no host is specified. 80func targetHost(c CommandLine, api libmachine.API) (string, error) { 81 if len(c.Args()) == 0 { 82 defaultExists, err := api.Exists(defaultMachineName) 83 if err != nil { 84 return "", fmt.Errorf("Error checking if host %q exists: %s", defaultMachineName, err) 85 } 86 87 if defaultExists { 88 return defaultMachineName, nil 89 } 90 91 return "", ErrNoDefault 92 } 93 94 return c.Args()[0], nil 95} 96 97func runAction(actionName string, c CommandLine, api libmachine.API) error { 98 var ( 99 hostsToLoad []string 100 ) 101 102 // If user did not specify a machine name explicitly, use the 'default' 103 // machine if it exists. This allows short form commands such as 104 // 'docker-machine stop' for convenience. 105 if len(c.Args()) == 0 { 106 target, err := targetHost(c, api) 107 if err != nil { 108 return err 109 } 110 111 hostsToLoad = []string{target} 112 } else { 113 hostsToLoad = c.Args() 114 } 115 116 hosts, hostsInError := persist.LoadHosts(api, hostsToLoad) 117 118 if len(hostsInError) > 0 { 119 errs := []error{} 120 for _, err := range hostsInError { 121 errs = append(errs, err) 122 } 123 return consolidateErrs(errs) 124 } 125 126 if len(hosts) == 0 { 127 return ErrHostLoad 128 } 129 130 if errs := runActionForeachMachine(actionName, hosts); len(errs) > 0 { 131 return consolidateErrs(errs) 132 } 133 134 for _, h := range hosts { 135 if err := api.Save(h); err != nil { 136 return fmt.Errorf("Error saving host to store: %s", err) 137 } 138 } 139 140 return nil 141} 142 143func runCommand(command func(commandLine CommandLine, api libmachine.API) error) func(context *cli.Context) { 144 return func(context *cli.Context) { 145 api := libmachine.NewClient(mcndirs.GetBaseDir(), mcndirs.GetMachineCertDir()) 146 defer api.Close() 147 148 if context.GlobalBool("native-ssh") { 149 api.SSHClientType = ssh.Native 150 } 151 api.GithubAPIToken = context.GlobalString("github-api-token") 152 api.Filestore.Path = context.GlobalString("storage-path") 153 154 // TODO (nathanleclaire): These should ultimately be accessed 155 // through the libmachine client by the rest of the code and 156 // not through their respective modules. For now, however, 157 // they are also being set the way that they originally were 158 // set to preserve backwards compatibility. 159 mcndirs.BaseDir = api.Filestore.Path 160 mcnutils.GithubAPIToken = api.GithubAPIToken 161 ssh.SetDefaultClient(api.SSHClientType) 162 163 if err := command(&contextCommandLine{context}, api); err != nil { 164 log.Error(err) 165 166 if crashErr, ok := err.(crashreport.CrashError); ok { 167 crashReporter := crashreport.NewCrashReporter(mcndirs.GetBaseDir(), context.GlobalString("bugsnag-api-token")) 168 crashReporter.Send(crashErr) 169 170 if _, ok := crashErr.Cause.(mcnerror.ErrDuringPreCreate); ok { 171 osExit(3) 172 return 173 } 174 } 175 176 osExit(1) 177 return 178 } 179 } 180} 181 182func confirmInput(msg string) (bool, error) { 183 fmt.Printf("%s (y/n): ", msg) 184 185 var resp string 186 _, err := fmt.Scanln(&resp) 187 if err != nil { 188 return false, err 189 } 190 191 confirmed := strings.Index(strings.ToLower(resp), "y") == 0 192 return confirmed, nil 193} 194 195var Commands = []cli.Command{ 196 { 197 Name: "active", 198 Usage: "Print which machine is active", 199 Action: runCommand(cmdActive), 200 Flags: []cli.Flag{ 201 cli.IntFlag{ 202 Name: "timeout, t", 203 Usage: fmt.Sprintf("Timeout in seconds, default to %ds", activeDefaultTimeout), 204 Value: activeDefaultTimeout, 205 }, 206 }, 207 }, 208 { 209 Name: "config", 210 Usage: "Print the connection config for machine", 211 Description: "Argument is a machine name.", 212 Action: runCommand(cmdConfig), 213 Flags: []cli.Flag{ 214 cli.BoolFlag{ 215 Name: "swarm", 216 Usage: "Display the Swarm config instead of the Docker daemon", 217 }, 218 }, 219 }, 220 { 221 Flags: SharedCreateFlags, 222 Name: "create", 223 Usage: "Create a machine", 224 Description: fmt.Sprintf("Run '%s create --driver name --help' to include the create flags for that driver in the help text.", os.Args[0]), 225 Action: runCommand(cmdCreateOuter), 226 SkipFlagParsing: true, 227 }, 228 { 229 Name: "env", 230 Usage: "Display the commands to set up the environment for the Docker client", 231 Description: "Argument is a machine name.", 232 Action: runCommand(cmdEnv), 233 Flags: []cli.Flag{ 234 cli.BoolFlag{ 235 Name: "swarm", 236 Usage: "Display the Swarm config instead of the Docker daemon", 237 }, 238 cli.StringFlag{ 239 Name: "shell", 240 Usage: "Force environment to be configured for a specified shell: [fish, cmd, powershell, tcsh, emacs], default is auto-detect", 241 }, 242 cli.BoolFlag{ 243 Name: "unset, u", 244 Usage: "Unset variables instead of setting them", 245 }, 246 cli.BoolFlag{ 247 Name: "no-proxy", 248 Usage: "Add machine IP to NO_PROXY environment variable", 249 }, 250 }, 251 }, 252 { 253 Name: "inspect", 254 Usage: "Inspect information about a machine", 255 Description: "Argument is a machine name.", 256 Action: runCommand(cmdInspect), 257 Flags: []cli.Flag{ 258 cli.StringFlag{ 259 Name: "format, f", 260 Usage: "Format the output using the given go template.", 261 Value: "", 262 }, 263 }, 264 }, 265 { 266 Name: "ip", 267 Usage: "Get the IP address of a machine", 268 Description: "Argument(s) are one or more machine names.", 269 Action: runCommand(cmdIP), 270 }, 271 { 272 Name: "kill", 273 Usage: "Kill a machine", 274 Description: "Argument(s) are one or more machine names.", 275 Action: runCommand(cmdKill), 276 }, 277 { 278 Name: "ls", 279 Usage: "List machines", 280 Action: runCommand(cmdLs), 281 Flags: []cli.Flag{ 282 cli.BoolFlag{ 283 Name: "quiet, q", 284 Usage: "Enable quiet mode", 285 }, 286 cli.StringSliceFlag{ 287 Name: "filter", 288 Usage: "Filter output based on conditions provided", 289 Value: &cli.StringSlice{}, 290 }, 291 cli.IntFlag{ 292 Name: "timeout, t", 293 Usage: fmt.Sprintf("Timeout in seconds, default to %ds", lsDefaultTimeout), 294 Value: lsDefaultTimeout, 295 }, 296 cli.StringFlag{ 297 Name: "format, f", 298 Usage: "Pretty-print machines using a Go template", 299 }, 300 }, 301 }, 302 { 303 Name: "provision", 304 Usage: "Re-provision existing machines", 305 Action: runCommand(cmdProvision), 306 }, 307 { 308 Name: "regenerate-certs", 309 Usage: "Regenerate TLS Certificates for a machine", 310 Description: "Argument(s) are one or more machine names.", 311 Action: runCommand(cmdRegenerateCerts), 312 Flags: []cli.Flag{ 313 cli.BoolFlag{ 314 Name: "force, f", 315 Usage: "Force rebuild and do not prompt", 316 }, 317 cli.BoolFlag{ 318 Name: "client-certs", 319 Usage: "Also regenerate client certificates and CA.", 320 }, 321 }, 322 }, 323 { 324 Name: "restart", 325 Usage: "Restart a machine", 326 Description: "Argument(s) are one or more machine names.", 327 Action: runCommand(cmdRestart), 328 }, 329 { 330 Flags: []cli.Flag{ 331 cli.BoolFlag{ 332 Name: "force, f", 333 Usage: "Remove local configuration even if machine cannot be removed, also implies an automatic yes (`-y`)", 334 }, 335 cli.BoolFlag{ 336 Name: "y", 337 Usage: "Assumes automatic yes to proceed with remove, without prompting further user confirmation", 338 }, 339 }, 340 Name: "rm", 341 Usage: "Remove a machine", 342 Description: "Argument(s) are one or more machine names.", 343 Action: runCommand(cmdRm), 344 }, 345 { 346 Name: "ssh", 347 Usage: "Log into or run a command on a machine with SSH.", 348 Description: "Arguments are [machine-name] [command]", 349 Action: runCommand(cmdSSH), 350 SkipFlagParsing: true, 351 }, 352 { 353 Name: "scp", 354 Usage: "Copy files between machines", 355 Description: "Arguments are [[user@]machine:][path] [[user@]machine:][path].", 356 Action: runCommand(cmdScp), 357 Flags: []cli.Flag{ 358 cli.BoolFlag{ 359 Name: "recursive, r", 360 Usage: "Copy files recursively (required to copy directories)", 361 }, 362 cli.BoolFlag{ 363 Name: "delta, d", 364 Usage: "Reduce amount of data sent over network by sending only the differences (uses rsync)", 365 }, 366 cli.BoolFlag{ 367 Name: "quiet, q", 368 Usage: "Disables the progress meter as well as warning and diagnostic messages from ssh", 369 }, 370 }, 371 }, 372 { 373 Name: "mount", 374 Usage: "Mount or unmount a directory from a machine with SSHFS.", 375 Description: "Arguments are [machine:][path] [mountpoint]", 376 Action: runCommand(cmdMount), 377 Flags: []cli.Flag{ 378 cli.BoolFlag{ 379 Name: "unmount, u", 380 Usage: "Unmount instead of mount", 381 }, 382 }, 383 }, 384 { 385 Name: "start", 386 Usage: "Start a machine", 387 Description: "Argument(s) are one or more machine names.", 388 Action: runCommand(cmdStart), 389 }, 390 { 391 Name: "status", 392 Usage: "Get the status of a machine", 393 Description: "Argument is a machine name.", 394 Action: runCommand(cmdStatus), 395 }, 396 { 397 Name: "stop", 398 Usage: "Stop a machine", 399 Description: "Argument(s) are one or more machine names.", 400 Action: runCommand(cmdStop), 401 }, 402 { 403 Name: "upgrade", 404 Usage: "Upgrade a machine to the latest version of Docker", 405 Description: "Argument(s) are one or more machine names.", 406 Action: runCommand(cmdUpgrade), 407 }, 408 { 409 Name: "url", 410 Usage: "Get the URL of a machine", 411 Description: "Argument is a machine name.", 412 Action: runCommand(cmdURL), 413 }, 414 { 415 Name: "version", 416 Usage: "Show the Docker Machine version or a machine docker version", 417 Action: runCommand(cmdVersion), 418 }, 419} 420 421func printIP(h *host.Host) func() error { 422 return func() error { 423 ip, err := h.Driver.GetIP() 424 if err != nil { 425 return fmt.Errorf("Error getting IP address: %s", err) 426 } 427 428 fmt.Println(ip) 429 430 return nil 431 } 432} 433 434// machineCommand maps the command name to the corresponding machine command. 435// We run commands concurrently and communicate back an error if there was one. 436func machineCommand(actionName string, host *host.Host, errorChan chan<- error) { 437 // TODO: These actions should have their own type. 438 commands := map[string](func() error){ 439 "configureAuth": host.ConfigureAuth, 440 "configureAllAuth": host.ConfigureAllAuth, 441 "start": host.Start, 442 "stop": host.Stop, 443 "restart": host.Restart, 444 "kill": host.Kill, 445 "upgrade": host.Upgrade, 446 "ip": printIP(host), 447 "provision": host.Provision, 448 } 449 450 log.Debugf("command=%s machine=%s", actionName, host.Name) 451 452 errorChan <- commands[actionName]() 453} 454 455// runActionForeachMachine will run the command across multiple machines 456func runActionForeachMachine(actionName string, machines []*host.Host) []error { 457 var ( 458 numConcurrentActions = 0 459 errorChan = make(chan error) 460 errs = []error{} 461 ) 462 463 for _, machine := range machines { 464 numConcurrentActions++ 465 go machineCommand(actionName, machine, errorChan) 466 } 467 468 // TODO: We should probably only do 5-10 of these 469 // at a time, since otherwise cloud providers might 470 // rate limit us. 471 for i := 0; i < numConcurrentActions; i++ { 472 if err := <-errorChan; err != nil { 473 errs = append(errs, err) 474 } 475 } 476 477 close(errorChan) 478 479 return errs 480} 481 482func consolidateErrs(errs []error) error { 483 finalErr := "" 484 for _, err := range errs { 485 finalErr = fmt.Sprintf("%s\n%s", finalErr, err) 486 } 487 488 return errors.New(strings.TrimSpace(finalErr)) 489} 490