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