diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..01dee70615 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +machine diff --git a/.godir b/.godir new file mode 100644 index 0000000000..38ee32ee33 --- /dev/null +++ b/.godir @@ -0,0 +1 @@ +github.com/docker/machine diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..6ecf069e8e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,13 @@ +# Contributing to libtrust + +Want to hack on Machine? Awesome! Here are instructions to get you +started. + +Machine is a part of the [Docker](https://www.docker.com) project, and follows +the same rules and principles. If you're already familiar with the way +Docker does things, you'll feel right at home. + +Otherwise, go read +[Docker's contributions guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md). + +Happy hacking! diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..bc49367286 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,2 @@ +FROM golang:onbuild +ENTRYPOINT ["machine"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..27448585ad --- /dev/null +++ b/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2014 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MAINTAINERS b/MAINTAINERS new file mode 100644 index 0000000000..5f7d6b0001 --- /dev/null +++ b/MAINTAINERS @@ -0,0 +1 @@ +Ben Firshman (@bfirsh) diff --git a/README.md b/README.md new file mode 100644 index 0000000000..e9a32fab45 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# Docker Machine + +Machine makes it really easy to create Docker hosts on local hypervisors and cloud providers. It create servers, installs Docker on them, then configures the Docker client to talk to them. + +It works a bit like this: + + $ machine create -d virtualbox dev + [info] Downloading boot2docker... + [info] Creating SSH key... + [info] Creating VirtualBox VM... + [info] Starting VirtualBox VM... + [info] Waiting for VM to start... + [info] "dev" has been created and is now the active host. Docker commands will now run against that host. + + $ machine ls + NAME ACTIVE DRIVER STATE URL + dev * virtualbox Running tcp://192.168.99.100:2375 + + $ export DOCKER_HOST=`machine url` DOCKER_AUTH=identity + + $ docker run busybox echo hello world + Unable to find image 'busybox' locally + Pulling repository busybox + e72ac664f4f0: Download complete + 511136ea3c5a: Download complete + df7546f9f060: Download complete + e433a6c5b276: Download complete + hello world + + $ machine create -d digitalocean --digitalocean-access-token=... staging + [info] Creating SSH key... + [info] Creating Digital Ocean droplet... + [info] Waiting for SSH... + [info] "staging" has been created and is now the active host. Docker commands will now run against that host. + + $ machine ls + NAME ACTIVE DRIVER STATE URL + dev virtualbox Running tcp://192.168.99.108:2376 + staging * digitalocean Running tcp://104.236.37.134:2376 + +Machine creates Docker hosts that are secure by default. The connection between the client and daemon is encrypted and authenticated using new identity-based authentication. If you'd like to learn more about this, it is being worked on in [a pull request on Docker](https://github.com/docker/docker/pull/8265). + +## Try it out + +Machine is still in its early stages. If you'd like to try out a preview build, download it here: + + - Mac OS X: + - Linux: + +You will also need a version of Docker with identity authentication. Builds are available here: + + - Mac OS X: https://bfirsh.s3.amazonaws.com/docker/darwin/docker-1.3.1-dev-identity-auth + - Linux: https://bfirsh.s3.amazonaws.com/docker/linux/docker-1.3.1-dev-identity-auth + +## Drivers + +### VirtualBox + +Creates machines locally on [VirtualBox](https://www.virtualbox.org/). Requires VirtualBox to be installed. + +Options: + + - `--virtualbox-boot2docker-url`: The URL of the boot2docker image. Defaults to the latest available version. + - `--virtualbox-disk-size`: Size of disk for the host in MB. Default: `20000` + - `--virtualbox-memory`: Size of memory for the host in MB. Default: `1024` + +### Digital Ocean + +Creates machines on [Digital Ocean](https://www.digitalocean.com/). You need to create a personal access token under "Apps & API" in the Digital Ocean Control Panel and pass that to `machine create` with the `--digitalocean-access-token` option. + +Options: + + - `--digitalocean-access-token`: Your personal access token for the Digital Ocean API. + - `--digitalocean-image`: The name of the Digital Ocean image to use. Default: `docker` + - `--digitalocean-region`: The region to create the droplet in. Default: `nyc3` + - `--digitalocean-size`: The size of the Digital Ocean driver. Default: `512mb` + +### Microsoft Azure + +Create machines on [Microsoft Azure](http://azure.microsoft.com/). + +You need to create a subscription with a cert. Run these commands: + + $ openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout mycert.pem -out mycert.pem + $ openssl pkcs12 -export -out mycert.pfx -in mycert.pem -name "My Certificate" + $ openssl x509 -inform pem -in mycert.pem -outform der -out mycert.cer + +Go to the Azure portal, go to the "Settings" page, then "Manage Certificates" and upload `mycert.cer`. + +Grab your subscription ID from the portal, then run `machine create` with these details: + + $ machine create -d azure --azure-subscription-id="SUB_ID" --azure-subscription-cert="mycert.cer" + +Options: + + - `--azure-subscription-id`: Your Azure subscription ID. + - `--azure-subscription-cert`: Your Azure subscription cert. + +## Contributing + +Want to hack on Machine? [Docker's contributions guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md) apply, and you'll also need [Docker with identity authentication](https://github.com/dmcgowan/docker/tree/tls_libtrust_auth) checked out in your GOPATH. + +## Creators + +**Ben Firshman** + +- +- + diff --git a/commands.go b/commands.go new file mode 100644 index 0000000000..b461c99edd --- /dev/null +++ b/commands.go @@ -0,0 +1,498 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "reflect" + "strings" + "sync" + "text/tabwriter" + + log "github.com/Sirupsen/logrus" + flag "github.com/docker/docker/pkg/mflag" + + "github.com/docker/machine/drivers" + _ "github.com/docker/machine/drivers/azure" + _ "github.com/docker/machine/drivers/digitalocean" + _ "github.com/docker/machine/drivers/none" + _ "github.com/docker/machine/drivers/virtualbox" +) + +type DockerCli struct{} + +func (cli *DockerCli) getMethod(args ...string) (func(...string) error, bool) { + camelArgs := make([]string, len(args)) + for i, s := range args { + if len(s) == 0 { + return nil, false + } + camelArgs[i] = strings.ToUpper(s[:1]) + strings.ToLower(s[1:]) + } + methodName := "Cmd" + strings.Join(camelArgs, "") + method := reflect.ValueOf(cli).MethodByName(methodName) + if !method.IsValid() { + return nil, false + } + return method.Interface().(func(...string) error), true +} + +func (cli *DockerCli) Cmd(args ...string) error { + if len(args) > 1 { + method, exists := cli.getMethod(args[:2]...) + if exists { + return method(args[2:]...) + } + } + if len(args) > 0 { + method, exists := cli.getMethod(args[0]) + if !exists { + fmt.Println("Error: Command not found:", args[0]) + return cli.CmdHelp() + } + return method(args[1:]...) + } + return cli.CmdHelp() +} + +func (cli *DockerCli) Subcmd(name, signature, description string) *flag.FlagSet { + flags := flag.NewFlagSet(name, flag.ContinueOnError) + flags.Usage = func() { + options := "" + if flags.FlagCountUndeprecated() > 0 { + options = "[OPTIONS] " + } + fmt.Fprintf(os.Stderr, "\nUsage: docker %s %s%s\n\n%s\n\n", name, options, signature, description) + flags.PrintDefaults() + os.Exit(2) + } + return flags +} + +func (cli *DockerCli) CmdHelp(args ...string) error { + if len(args) > 1 { + method, exists := cli.getMethod(args[:2]...) + if exists { + method("--help") + return nil + } + } + if len(args) > 0 { + method, exists := cli.getMethod(args[0]) + if !exists { + fmt.Fprintf(os.Stderr, "Error: Command not found: %s\n", args[0]) + } else { + method("--help") + return nil + } + } + + flag.Usage() + + return nil +} + +func (cli *DockerCli) CmdLs(args ...string) error { + cmd := cli.Subcmd("machines ls", "", "List machines") + quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only display names") + + if err := cmd.Parse(args); err != nil { + return err + } + + store := NewStore() + + hostList, err := store.List() + if err != nil { + return err + } + + w := tabwriter.NewWriter(os.Stdout, 5, 1, 3, ' ', 0) + + if !*quiet { + fmt.Fprintln(w, "NAME\tACTIVE\tDRIVER\tSTATE\tURL") + } + + wg := sync.WaitGroup{} + + for _, host := range hostList { + host := host + if *quiet { + fmt.Fprintf(w, "%s\n", host.Name) + } else { + wg.Add(1) + go func() { + currentState, err := host.Driver.GetState() + if err != nil { + log.Errorf("error getting state for host %s: %s", host.Name, err) + } + + url, err := host.GetURL() + if err != nil { + if err == drivers.ErrHostIsNotRunning { + url = "" + } else { + log.Errorf("error getting URL for host %s: %s", host.Name, err) + } + } + + isActive, err := store.IsActive(&host) + if err != nil { + log.Errorf("error determining whether host %q is active: %s", + host.Name, err) + } + + activeString := "" + if isActive { + activeString = "*" + } + + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", + host.Name, activeString, host.Driver.DriverName(), currentState, url) + wg.Done() + }() + } + } + + wg.Wait() + w.Flush() + + return nil +} + +func (cli *DockerCli) CmdCreate(args ...string) error { + cmd := cli.Subcmd("machines create", "NAME", "Create machines") + + driverDesc := fmt.Sprintf( + "Driver to create machine with. Available drivers: %s", + strings.Join(drivers.GetDriverNames(), ", "), + ) + + driver := cmd.String([]string{"d", "-driver"}, "none", driverDesc) + + createFlags := drivers.RegisterCreateFlags(cmd) + + if err := cmd.Parse(args); err != nil { + return err + } + if cmd.NArg() != 1 { + cmd.Usage() + return nil + } + + name := cmd.Arg(0) + + store := NewStore() + + driverCreateFlags, _ := createFlags[*driver] + host, err := store.Create(name, *driver, driverCreateFlags) + if err != nil { + return err + } + if err := store.SetActive(host); err != nil { + return err + } + log.Infof("%q has been created and is now the active machine. Docker commands will now run against that machine.", name) + return nil +} + +func (cli *DockerCli) CmdStart(args ...string) error { + cmd := cli.Subcmd("machines start", "NAME", "Start a machine") + if err := cmd.Parse(args); err != nil { + return err + } + if cmd.NArg() < 1 { + cmd.Usage() + return nil + } + + store := NewStore() + + host, err := store.Load(cmd.Arg(0)) + if err != nil { + return err + } + return host.Start() +} + +func (cli *DockerCli) CmdStop(args ...string) error { + cmd := cli.Subcmd("machines stop", "NAME", "Stop a machine") + if err := cmd.Parse(args); err != nil { + return err + } + if cmd.NArg() < 1 { + cmd.Usage() + return nil + } + + store := NewStore() + + host, err := store.Load(cmd.Arg(0)) + if err != nil { + return err + } + return host.Stop() +} + +func (cli *DockerCli) CmdRm(args ...string) error { + cmd := cli.Subcmd("machines rm", "NAME", "Remove a machine") + force := cmd.Bool([]string{"f", "-force"}, false, "Remove local configuration even if machine cannot be removed") + + if err := cmd.Parse(args); err != nil { + return err + } + if cmd.NArg() < 1 { + cmd.Usage() + return nil + } + + isError := false + + store := NewStore() + for _, host := range cmd.Args() { + host := host + if err := store.Remove(host, *force); err != nil { + log.Errorf("Error removing machine %s: %s", host, err) + isError = true + } + } + if isError { + return fmt.Errorf("There was an error removing a machine. To force remove it, pass the -f option. Warning: this might leave it running on the provider.") + } + return nil +} + +func (cli *DockerCli) CmdIp(args ...string) error { + cmd := cli.Subcmd("machines ip", "NAME", "Get the IP address of a machine") + if err := cmd.Parse(args); err != nil { + return err + } + if cmd.NArg() > 1 { + cmd.Usage() + return nil + } + + var ( + err error + host *Host + store = NewStore() + ) + + if cmd.NArg() == 1 { + host, err = store.Load(cmd.Arg(0)) + if err != nil { + return err + } + } else { + host, err = store.GetActive() + if err != nil { + return err + } + if host == nil { + os.Exit(1) + } + } + + ip, err := host.Driver.GetIP() + if err != nil { + return err + } + + fmt.Println(ip) + + return nil +} + +func (cli *DockerCli) CmdUrl(args ...string) error { + cmd := cli.Subcmd("machines url", "NAME", "Get the URL of a machine") + if err := cmd.Parse(args); err != nil { + return err + } + if cmd.NArg() > 1 { + cmd.Usage() + return nil + } + + var ( + err error + host *Host + store = NewStore() + ) + + if cmd.NArg() == 1 { + host, err = store.Load(cmd.Arg(0)) + if err != nil { + return err + } + } else { + host, err = store.GetActive() + if err != nil { + return err + } + if host == nil { + os.Exit(1) + } + } + + url, err := host.GetURL() + if err != nil { + return err + } + + fmt.Println(url) + + return nil +} + +func (cli *DockerCli) CmdRestart(args ...string) error { + cmd := cli.Subcmd("machines restart", "NAME", "Restart a machine") + if err := cmd.Parse(args); err != nil { + return err + } + if cmd.NArg() < 1 { + cmd.Usage() + return nil + } + + store := NewStore() + + host, err := store.Load(cmd.Arg(0)) + if err != nil { + return err + } + return host.Driver.Restart() +} + +func (cli *DockerCli) CmdKill(args ...string) error { + cmd := cli.Subcmd("machines kill", "NAME", "Kill a machine") + if err := cmd.Parse(args); err != nil { + return err + } + if cmd.NArg() < 1 { + cmd.Usage() + return nil + } + + store := NewStore() + + host, err := store.Load(cmd.Arg(0)) + if err != nil { + return err + } + return host.Driver.Kill() +} + +func (cli *DockerCli) CmdSsh(args ...string) error { + cmd := cli.Subcmd("machines ssh", "NAME [COMMAND ...]", "Log into or run a command on a machine with SSH") + if err := cmd.Parse(args); err != nil { + return err + } + + if cmd.NArg() < 1 { + cmd.Usage() + return nil + } + + i := 1 + for i < len(os.Args) && os.Args[i-1] != cmd.Arg(0) { + i++ + } + + store := NewStore() + + host, err := store.Load(cmd.Arg(0)) + if err != nil { + return err + } + + sshCmd, err := host.Driver.GetSSHCommand(os.Args[i:]...) + if err != nil { + return err + } + sshCmd.Stdin = os.Stdin + sshCmd.Stdout = os.Stdout + sshCmd.Stderr = os.Stderr + if err := sshCmd.Run(); err != nil { + return fmt.Errorf("%s", err) + } + return nil +} + +func (cli *DockerCli) CmdActive(args ...string) error { + cmd := cli.Subcmd("machines active", "[NAME]", "Get or set the active machine") + if err := cmd.Parse(args); err != nil { + return err + } + + store := NewStore() + + if cmd.NArg() == 0 { + host, err := store.GetActive() + if err != nil { + return err + } + if host != nil { + fmt.Println(host.Name) + } + } else if cmd.NArg() == 1 { + host, err := store.Load(cmd.Arg(0)) + if err != nil { + return err + } + if err := store.SetActive(host); err != nil { + return err + } + } else { + cmd.Usage() + } + + return nil + +} + +func (cli *DockerCli) CmdInspect(args ...string) error { + cmd := cli.Subcmd("machines inspect", "[NAME]", "Get detailed information about a machine") + if err := cmd.Parse(args); err != nil { + return err + } + + if cmd.NArg() == 0 { + cmd.Usage() + return nil + } + + store := NewStore() + host, err := store.Load(cmd.Arg(0)) + if err != nil { + return err + } + + prettyJson, err := json.MarshalIndent(host, "", " ") + if err != nil { + return err + } + + fmt.Println(string(prettyJson)) + + return nil +} + +func (cli *DockerCli) CmdUpgrade(args ...string) error { + cmd := cli.Subcmd("machines upgrade", "[NAME]", "Upgrade a machine to the latest version of Docker") + if err := cmd.Parse(args); err != nil { + return err + } + + if cmd.NArg() == 0 { + cmd.Usage() + return nil + } + + store := NewStore() + host, err := store.Load(cmd.Arg(0)) + if err != nil { + return err + } + + return host.Driver.Upgrade() +} diff --git a/drivers/azure/azure.go b/drivers/azure/azure.go new file mode 100644 index 0000000000..a6a0cdfdf5 --- /dev/null +++ b/drivers/azure/azure.go @@ -0,0 +1,494 @@ +package azure + +import ( + "fmt" + "net" + "os" + "os/exec" + "path" + "strconv" + "strings" + "time" + + azure "github.com/MSOpenTech/azure-sdk-for-go" + "github.com/MSOpenTech/azure-sdk-for-go/clients/vmClient" + + log "github.com/Sirupsen/logrus" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/utils" + "github.com/docker/machine/drivers" + "github.com/docker/machine/ssh" + "github.com/docker/machine/state" +) + +type Driver struct { + SubscriptionID string + SubscriptionCert string + PublishSettingsFilePath string + Name string + Location string + Size string + UserName string + UserPassword string + Image string + SSHPort int + DockerPort int + storePath string +} + +type CreateFlags struct { + SubscriptionID *string + SubscriptionCert *string + PublishSettingsFilePath *string + Name *string + Location *string + Size *string + UserName *string + UserPassword *string + Image *string + SSHPort *string + DockerPort *string +} + +func init() { + drivers.Register("azure", &drivers.RegisteredDriver{ + New: NewDriver, + RegisterCreateFlags: RegisterCreateFlags, + }) +} + +// RegisterCreateFlags registers the flags this driver adds to +// "docker hosts create" +func RegisterCreateFlags(cmd *flag.FlagSet) interface{} { + createFlags := new(CreateFlags) + createFlags.SubscriptionID = cmd.String( + []string{"-azure-subscription-id"}, + "", + "Azure subscription ID", + ) + createFlags.SubscriptionCert = cmd.String( + []string{"-azure-subscription-cert"}, + "", + "Azure subscription cert", + ) + createFlags.PublishSettingsFilePath = cmd.String( + []string{"-azure-publish-settings-file"}, + "", + "Azure publish settings file", + ) + createFlags.Location = cmd.String( + []string{"-azure-location"}, + "West US", + "Azure location", + ) + createFlags.Size = cmd.String( + []string{"-azure-size"}, + "Small", + "Azure size", + ) + createFlags.Name = cmd.String( + []string{"-azure-name"}, + "", + "Azure cloud service name", + ) + createFlags.UserName = cmd.String( + []string{"-azure-username"}, + "tcuser", + "Azure username", + ) + createFlags.UserPassword = cmd.String( + []string{"-azure-password"}, + "", + "Azure user password", + ) + createFlags.Image = cmd.String( + []string{"-azure-image"}, + "", + "Azure image name. Default is Ubuntu 14.04 LTS x64", + ) + createFlags.SSHPort = cmd.String( + []string{"-azure-ssh"}, + "22", + "Azure ssh port", + ) + createFlags.DockerPort = cmd.String( + []string{"-azure-docker-port"}, + "4243", + "Azure docker port", + ) + return createFlags +} + +func NewDriver(storePath string) (drivers.Driver, error) { + driver := &Driver{storePath: storePath} + return driver, nil +} + +func (driver *Driver) DriverName() string { + return "azure" +} + +func (driver *Driver) SetConfigFromFlags(flagsInterface interface{}) error { + flags := flagsInterface.(*CreateFlags) + driver.SubscriptionID = *flags.SubscriptionID + + if *flags.SubscriptionCert != "" { + if _, err := os.Stat(*flags.SubscriptionCert); os.IsNotExist(err) { + return err + } + driver.SubscriptionCert = *flags.SubscriptionCert + } + + if *flags.PublishSettingsFilePath != "" { + if _, err := os.Stat(*flags.PublishSettingsFilePath); os.IsNotExist(err) { + return err + } + driver.PublishSettingsFilePath = *flags.PublishSettingsFilePath + } + + if (driver.SubscriptionID == "" || driver.SubscriptionCert == "") && driver.PublishSettingsFilePath == "" { + return fmt.Errorf("Please specify azure subscription params using options: --azure-subscription-id and --azure-subscription-cert or --azure-publish-settings-file") + } + + if *flags.Name == "" { + driver.Name = generateVMName() + } else { + driver.Name = *flags.Name + } + + if *flags.Image == "" { + driver.Image = "b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-14_04_1-LTS-amd64-server-20140927-en-us-30GB" + } else { + driver.Image = *flags.Image + } + + driver.Location = *flags.Location + driver.Size = *flags.Size + + if strings.ToLower(*flags.UserName) == "docker" { + return fmt.Errorf("'docker' is not valid user name for docker host. Please specify another user name") + } + + driver.UserName = *flags.UserName + driver.UserPassword = *flags.UserPassword + + dockerPort, err := strconv.Atoi(*flags.DockerPort) + if err != nil { + return err + } + driver.DockerPort = dockerPort + + sshPort, err := strconv.Atoi(*flags.SSHPort) + if err != nil { + return err + } + driver.SSHPort = sshPort + + return nil +} + +func (driver *Driver) Create() error { + if err := driver.setUserSubscription(); err != nil { + return err + } + + log.Infof("Creating Azure host...") + vmConfig, err := vmClient.CreateAzureVMConfiguration(driver.Name, driver.Size, driver.Image, driver.Location) + if err != nil { + return err + } + + if err := driver.generateCertForAzure(); err != nil { + return err + } + + vmConfig, err = vmClient.AddAzureLinuxProvisioningConfig(vmConfig, driver.UserName, driver.UserPassword, driver.azureCertPath(), driver.SSHPort) + if err != nil { + return err + } + + vmConfig, err = vmClient.SetAzureDockerVMExtension(vmConfig, driver.DockerPort, "0.4") + if err != nil { + return err + } + + if err := vmClient.CreateAzureVM(vmConfig, driver.Name, driver.Location); err != nil { + return err + } + + if err := driver.waitForSSH(); err != nil { + return err + } + + if err := driver.waitForDocker(); err != nil { + return err + } + + return nil +} + +func (driver *Driver) GetURL() (string, error) { + url := fmt.Sprintf("tcp://%s.cloudapp.net:%v", driver.Name, driver.DockerPort) + return url, nil +} + +func (driver *Driver) GetIP() (string, error) { + return fmt.Sprintf("%s.cloudapp.net", driver.Name), nil +} + +func (driver *Driver) GetState() (state.State, error) { + err := driver.setUserSubscription() + if err != nil { + return state.Error, err + } + + dockerVM, err := vmClient.GetVMDeployment(driver.Name, driver.Name) + if err != nil { + if strings.Contains(err.Error(), "Code: ResourceNotFound") { + return state.Error, fmt.Errorf("Azure host was not found. Please check your Azure subscription.") + } + + return state.Error, err + } + + vmState := dockerVM.RoleInstanceList.RoleInstance[0].PowerState + switch vmState { + case "Started": + return state.Running, nil + case "Starting": + return state.Starting, nil + case "Stopped": + return state.Stopped, nil + } + + return state.None, nil +} + +func (driver *Driver) Start() error { + err := driver.setUserSubscription() + if err != nil { + return err + } + + vmState, err := driver.GetState() + if err != nil { + return err + } + if vmState == state.Running || vmState == state.Starting { + log.Infof("Host is already running or starting") + return nil + } + + err = vmClient.StartRole(driver.Name, driver.Name, driver.Name) + if err != nil { + return err + } + err = driver.waitForSSH() + if err != nil { + return err + } + err = driver.waitForDocker() + if err != nil { + return err + } + return nil +} + +func (driver *Driver) Stop() error { + err := driver.setUserSubscription() + if err != nil { + return err + } + vmState, err := driver.GetState() + if err != nil { + return err + } + if vmState == state.Stopped { + log.Infof("Host is already stopped") + return nil + } + err = vmClient.ShutdownRole(driver.Name, driver.Name, driver.Name) + if err != nil { + return err + } + return nil +} + +func (driver *Driver) Remove() error { + err := driver.setUserSubscription() + if err != nil { + return err + } + + _, err = vmClient.GetVMDeployment(driver.Name, driver.Name) + if err != nil { + if strings.Contains(err.Error(), "Code: ResourceNotFound") { + return nil + } + + return err + } + + err = vmClient.DeleteVMDeployment(driver.Name, driver.Name) + if err != nil { + return err + } + err = vmClient.DeleteHostedService(driver.Name) + if err != nil { + return err + } + return nil +} + +func (driver *Driver) Restart() error { + err := driver.setUserSubscription() + if err != nil { + return err + } + vmState, err := driver.GetState() + if err != nil { + return err + } + if vmState == state.Stopped { + return fmt.Errorf("Host is already stopped, use start command to run it") + } + err = vmClient.RestartRole(driver.Name, driver.Name, driver.Name) + if err != nil { + return err + } + err = driver.waitForSSH() + if err != nil { + return err + } + err = driver.waitForDocker() + if err != nil { + return err + } + return nil +} + +func (driver *Driver) Kill() error { + err := driver.setUserSubscription() + if err != nil { + return err + } + vmState, err := driver.GetState() + if err != nil { + return err + } + if vmState == state.Stopped { + log.Infof("Host is already stopped") + return nil + } + err = vmClient.ShutdownRole(driver.Name, driver.Name, driver.Name) + if err != nil { + return err + } + return nil +} + +func (driver *Driver) GetSSHCommand(args ...string) (*exec.Cmd, error) { + err := driver.setUserSubscription() + if err != nil { + return nil, err + } + + vmState, err := driver.GetState() + if err != nil { + return nil, err + } + + if vmState == state.Stopped { + return nil, fmt.Errorf("Azure host is stopped. Please start it before using ssh command.") + } + + return ssh.GetSSHCommand(driver.Name+".cloudapp.net", driver.SSHPort, driver.UserName, driver.sshKeyPath(), args...), nil +} + +func (driver *Driver) Upgrade() error { + return nil +} + +func generateVMName() string { + randomID := utils.TruncateID(utils.GenerateRandomID()) + return fmt.Sprintf("docker-host-%s", randomID) +} + +func (driver *Driver) setUserSubscription() error { + if len(driver.PublishSettingsFilePath) != 0 { + err := azure.ImportPublishSettingsFile(driver.PublishSettingsFilePath) + if err != nil { + return err + } + return nil + } + err := azure.ImportPublishSettings(driver.SubscriptionID, driver.SubscriptionCert) + if err != nil { + return err + } + return nil +} + +func (driver *Driver) waitForSSH() error { + log.Infof("Waiting for SSH...") + err := ssh.WaitForTCP(fmt.Sprintf("%s:%v", driver.Name+".cloudapp.net", driver.SSHPort)) + if err != nil { + return err + } + + return nil +} + +func (driver *Driver) waitForDocker() error { + log.Infof("Waiting for docker daemon on host to be available...") + maxRepeats := 48 + url := fmt.Sprintf("%s:%v", driver.Name+".cloudapp.net", driver.DockerPort) + success := waitForDockerEndpoint(url, maxRepeats) + if !success { + return fmt.Errorf("Can not run docker daemon on remote machine. Please try again.") + } + return nil +} + +func waitForDockerEndpoint(url string, maxRepeats int) bool { + counter := 0 + for { + conn, err := net.Dial("tcp", url) + if err != nil { + time.Sleep(10 * time.Second) + counter++ + if counter == maxRepeats { + return false + } + continue + } + defer conn.Close() + break + } + return true +} + +func (driver *Driver) generateCertForAzure() error { + if err := ssh.GenerateSSHKey(driver.sshKeyPath()); err != nil { + return err + } + + cmd := exec.Command("openssl", "req", "-x509", "-key", driver.sshKeyPath(), "-nodes", "-days", "365", "-newkey", "rsa:2048", "-out", driver.azureCertPath(), "-subj", "/C=AU/ST=Some-State/O=InternetWidgitsPtyLtd/CN=\\*") + if err := cmd.Run(); err != nil { + return err + } + + return nil +} + +func (driver *Driver) sshKeyPath() string { + return path.Join(driver.storePath, "id_rsa") +} + +func (driver *Driver) publicSSHKeyPath() string { + return driver.sshKeyPath() + ".pub" +} + +func (driver *Driver) azureCertPath() string { + return path.Join(driver.storePath, "azure_cert.pem") +} diff --git a/drivers/digitalocean/digitalocean.go b/drivers/digitalocean/digitalocean.go new file mode 100644 index 0000000000..2ef263270f --- /dev/null +++ b/drivers/digitalocean/digitalocean.go @@ -0,0 +1,331 @@ +package digitalocean + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "time" + + "code.google.com/p/goauth2/oauth" + log "github.com/Sirupsen/logrus" + "github.com/digitalocean/godo" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/utils" + "github.com/docker/machine/drivers" + "github.com/docker/machine/ssh" + "github.com/docker/machine/state" +) + +type Driver struct { + AccessToken string + DropletID int + DropletName string + Image string + IPAddress string + Region string + SSHKeyID int + Size string + storePath string +} + +type CreateFlags struct { + AccessToken *string + Image *string + Region *string + Size *string +} + +func init() { + drivers.Register("digitalocean", &drivers.RegisteredDriver{ + New: NewDriver, + RegisterCreateFlags: RegisterCreateFlags, + }) +} + +// RegisterCreateFlags registers the flags this driver adds to +// "docker hosts create" +func RegisterCreateFlags(cmd *flag.FlagSet) interface{} { + createFlags := new(CreateFlags) + createFlags.AccessToken = cmd.String( + []string{"-digitalocean-access-token"}, + "", + "Digital Ocean access token", + ) + createFlags.Image = cmd.String( + []string{"-digitalocean-image"}, + "docker", + "Digital Ocean image", + ) + createFlags.Region = cmd.String( + []string{"-digitalocean-region"}, + "nyc3", + "Digital Ocean region", + ) + createFlags.Size = cmd.String( + []string{"-digitalocean-size"}, + "512mb", + "Digital Ocean size", + ) + return createFlags +} + +func NewDriver(storePath string) (drivers.Driver, error) { + return &Driver{storePath: storePath}, nil +} + +func (d *Driver) DriverName() string { + return "digitalocean" +} + +func (d *Driver) SetConfigFromFlags(flagsInterface interface{}) error { + flags := flagsInterface.(*CreateFlags) + d.AccessToken = *flags.AccessToken + d.Image = *flags.Image + d.Region = *flags.Region + d.Size = *flags.Size + + if d.AccessToken == "" { + return fmt.Errorf("digitalocean driver requires the --digitalocean-access-token option") + } + + return nil +} + +func (d *Driver) Create() error { + d.setDropletNameIfNotSet() + + log.Infof("Creating SSH key...") + + key, err := d.createSSHKey() + if err != nil { + return err + } + + d.SSHKeyID = key.ID + + log.Infof("Creating Digital Ocean droplet...") + + client := d.getClient() + + createRequest := &godo.DropletCreateRequest{ + Image: d.Image, + Name: d.DropletName, + Region: d.Region, + Size: d.Size, + SSHKeys: []interface{}{d.SSHKeyID}, + } + + newDroplet, _, err := client.Droplets.Create(createRequest) + if err != nil { + return err + } + + d.DropletID = newDroplet.Droplet.ID + + for { + newDroplet, _, err = client.Droplets.Get(d.DropletID) + if err != nil { + return err + } + for _, network := range newDroplet.Droplet.Networks.V4 { + if network.Type == "public" { + d.IPAddress = network.IPAddress + } + } + + if d.IPAddress != "" { + break + } + + time.Sleep(1 * time.Second) + } + + log.Debugf("Created droplet ID %d, IP address %s", + newDroplet.Droplet.ID, + d.IPAddress) + + log.Infof("Waiting for SSH...") + + if err := ssh.WaitForTCP(fmt.Sprintf("%s:%d", d.IPAddress, 22)); err != nil { + return err + } + + log.Debugf("HACK: Downloading version of Docker with identity auth...") + + cmd, err := d.GetSSHCommand("stop docker") + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + + cmd, err = d.GetSSHCommand("curl -sS https://bfirsh.s3.amazonaws.com/docker/docker-1.3.1-dev-identity-auth > /usr/bin/docker") + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + + log.Debugf("Updating /etc/default/docker to use identity auth...") + + cmd, err = d.GetSSHCommand("echo 'export DOCKER_OPTS=\"--auth=identity --host=tcp://0.0.0.0:2376\"' >> /etc/default/docker") + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + + log.Debugf("Adding key to authorized-keys.d...") + + if err := drivers.AddPublicKeyToAuthorizedHosts(d, "/.docker/authorized-keys.d"); err != nil { + return err + } + + cmd, err = d.GetSSHCommand("start docker") + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + + return nil +} + +func (d *Driver) createSSHKey() (*godo.Key, error) { + if err := ssh.GenerateSSHKey(d.sshKeyPath()); err != nil { + return nil, err + } + + publicKey, err := ioutil.ReadFile(d.publicSSHKeyPath()) + if err != nil { + return nil, err + } + + createRequest := &godo.KeyCreateRequest{ + Name: d.DropletName, + PublicKey: string(publicKey), + } + + key, _, err := d.getClient().Keys.Create(createRequest) + if err != nil { + return key, err + } + + return key, nil +} + +func (d *Driver) GetURL() (string, error) { + ip, err := d.GetIP() + if err != nil { + return "", err + } + return fmt.Sprintf("tcp://%s:2376", ip), nil +} + +func (d *Driver) GetIP() (string, error) { + if d.IPAddress == "" { + return "", fmt.Errorf("IP address is not set") + } + return d.IPAddress, nil +} + +func (d *Driver) GetState() (state.State, error) { + droplet, _, err := d.getClient().Droplets.Get(d.DropletID) + if err != nil { + return state.Error, err + } + switch droplet.Droplet.Status { + case "new": + return state.Starting, nil + case "active": + return state.Running, nil + case "off": + return state.Stopped, nil + } + return state.None, nil +} + +func (d *Driver) Start() error { + _, _, err := d.getClient().DropletActions.PowerOn(d.DropletID) + return err +} + +func (d *Driver) Stop() error { + _, _, err := d.getClient().DropletActions.Shutdown(d.DropletID) + return err +} + +func (d *Driver) Remove() error { + client := d.getClient() + if resp, err := client.Keys.DeleteByID(d.SSHKeyID); err != nil { + if resp.StatusCode == 404 { + log.Infof("Digital Ocean SSH key doesn't exist, assuming it is already deleted") + } else { + return err + } + } + if resp, err := client.Droplets.Delete(d.DropletID); err != nil { + if resp.StatusCode == 404 { + log.Infof("Digital Ocean droplet doesn't exist, assuming it is already deleted") + } else { + return err + } + } + return nil +} + +func (d *Driver) Restart() error { + _, _, err := d.getClient().DropletActions.Reboot(d.DropletID) + return err +} + +func (d *Driver) Kill() error { + _, _, err := d.getClient().DropletActions.PowerOff(d.DropletID) + return err +} + +func (d *Driver) Upgrade() error { + sshCmd, err := d.GetSSHCommand("apt-get update && apt-get install lxc-docker") + if err != nil { + return err + } + sshCmd.Stdin = os.Stdin + sshCmd.Stdout = os.Stdout + sshCmd.Stderr = os.Stderr + if err := sshCmd.Run(); err != nil { + return fmt.Errorf("%s", err) + } + return nil +} + +func (d *Driver) GetSSHCommand(args ...string) (*exec.Cmd, error) { + return ssh.GetSSHCommand(d.IPAddress, 22, "root", d.sshKeyPath(), args...), nil +} + +func (d *Driver) setDropletNameIfNotSet() { + if d.DropletName == "" { + d.DropletName = fmt.Sprintf("docker-host-%s", utils.GenerateRandomID()) + } +} + +func (d *Driver) getClient() *godo.Client { + t := &oauth.Transport{ + Token: &oauth.Token{AccessToken: d.AccessToken}, + } + + return godo.NewClient(t.Client()) +} + +func (d *Driver) sshKeyPath() string { + return path.Join(d.storePath, "id_rsa") +} + +func (d *Driver) publicSSHKeyPath() string { + return d.sshKeyPath() + ".pub" +} diff --git a/drivers/drivers.go b/drivers/drivers.go new file mode 100644 index 0000000000..0718579b7f --- /dev/null +++ b/drivers/drivers.go @@ -0,0 +1,122 @@ +package drivers + +import ( + "errors" + "fmt" + "os/exec" + "sort" + + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/machine/state" +) + +// Driver defines how a host is created and controlled. Different types of +// driver represent different ways hosts can be created (e.g. different +// hypervisors, different cloud providers) +type Driver interface { + // DriverName returns the name of the driver as it is registered + DriverName() string + + // SetConfigFromFlags configures the driver with the object that was returned + // by RegisterCreateFlags + SetConfigFromFlags(flags interface{}) error + + // GetURL returns a Docker compatible host URL for connecting to this host + // e.g. tcp://1.2.3.4:2376 + GetURL() (string, error) + + // GetIP returns an IP or hostname that this host is available at + // e.g. 1.2.3.4 or docker-host-d60b70a14d3a.cloudapp.net + GetIP() (string, error) + + // GetState returns the state that the host is in (running, stopped, etc) + GetState() (state.State, error) + + // Create a host using the driver's config + Create() error + + // Remove a host + Remove() error + + // Start a host + Start() error + + // Stop a host gracefully + Stop() error + + // Restart a host. This may just call Stop(); Start() if the provider does not + // have any special restart behaviour. + Restart() error + + // Kill stops a host forcefully + Kill() error + + // Upgrade the version of Docker on the host to the latest version + Upgrade() error + + // GetSSHCommand returns a command for SSH pointing at the correct user, host + // and keys for the host with args appended. If no args are passed, it will + // initiate an interactive SSH session as if SSH were passed no args. + GetSSHCommand(args ...string) (*exec.Cmd, error) +} + +// RegisteredDriver is used to register a driver with the Register function. +// It has two attributes: +// - New: a function that returns a new driver given a path to store host +// configuration in +// - RegisterCreateFlags: a function that takes the FlagSet for +// "docker hosts create" and returns an object to pass to SetConfigFromFlags +type RegisteredDriver struct { + New func(storePath string) (Driver, error) + RegisterCreateFlags func(cmd *flag.FlagSet) interface{} +} + +var ErrHostIsNotRunning = errors.New("host is not running") + +var ( + drivers map[string]*RegisteredDriver +) + +func init() { + drivers = make(map[string]*RegisteredDriver) +} + +// Register a driver +func Register(name string, registeredDriver *RegisteredDriver) error { + if _, exists := drivers[name]; exists { + return fmt.Errorf("Name already registered %s", name) + } + drivers[name] = registeredDriver + + return nil +} + +// NewDriver creates a new driver of type "name" +func NewDriver(name string, storePath string) (Driver, error) { + driver, exists := drivers[name] + if !exists { + return nil, fmt.Errorf("hosts: Unknown driver %q", name) + } + return driver.New(storePath) +} + +// RegisterCreateFlags runs RegisterCreateFlags for all of the drivers and +// returns their return values indexed by the driver name +func RegisterCreateFlags(cmd *flag.FlagSet) map[string]interface{} { + flags := make(map[string]interface{}) + for driverName := range drivers { + driver := drivers[driverName] + flags[driverName] = driver.RegisterCreateFlags(cmd) + } + return flags +} + +// GetDriverNames returns a slice of all registered driver names +func GetDriverNames() []string { + names := make([]string, 0, len(drivers)) + for k := range drivers { + names = append(names, k) + } + sort.Strings(names) + return names +} diff --git a/drivers/none/none.go b/drivers/none/none.go new file mode 100644 index 0000000000..498fd08b47 --- /dev/null +++ b/drivers/none/none.go @@ -0,0 +1,102 @@ +package none + +import ( + "fmt" + "os/exec" + + "github.com/docker/docker/api" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/machine/drivers" + "github.com/docker/machine/state" +) + +// Driver is the driver used when no driver is selected. It is used to +// connect to existing Docker hosts by specifying the URL of the host as +// an option. +type Driver struct { + URL string +} + +type CreateFlags struct { + URL *string +} + +func init() { + drivers.Register("none", &drivers.RegisteredDriver{ + New: NewDriver, + RegisterCreateFlags: RegisterCreateFlags, + }) +} + +// RegisterCreateFlags registers the flags this driver adds to +// "docker hosts create" +func RegisterCreateFlags(cmd *flag.FlagSet) interface{} { + createFlags := new(CreateFlags) + createFlags.URL = cmd.String([]string{"-url"}, "", "URL of host when no driver is selected") + return createFlags +} + +func NewDriver(storePath string) (drivers.Driver, error) { + return &Driver{}, nil +} + +func (d *Driver) DriverName() string { + return "none" +} + +func (d *Driver) SetConfigFromFlags(flagsInterface interface{}) error { + flags := flagsInterface.(*CreateFlags) + if *flags.URL == "" { + return fmt.Errorf("--url option is required when no driver is selected") + } + url, err := api.ValidateHost(*flags.URL) + if err != nil { + return err + } + d.URL = url + return nil +} + +func (d *Driver) GetURL() (string, error) { + return d.URL, nil +} + +func (d *Driver) GetIP() (string, error) { + return "", nil +} + +func (d *Driver) GetState() (state.State, error) { + return state.None, nil +} + +func (d *Driver) Create() error { + return nil +} + +func (d *Driver) Start() error { + return fmt.Errorf("hosts without a driver cannot be started") +} + +func (d *Driver) Stop() error { + return fmt.Errorf("hosts without a driver cannot be stopped") +} + +func (d *Driver) Remove() error { + return nil +} + +func (d *Driver) Restart() error { + return fmt.Errorf("hosts without a driver cannot be restarted") +} + +func (d *Driver) Kill() error { + return fmt.Errorf("hosts without a driver cannot be killed") +} + +func (d *Driver) Upgrade() error { + return fmt.Errorf("hosts without a driver cannot be upgraded") +} + +func (d *Driver) GetSSHCommand(args ...string) (*exec.Cmd, error) { + return nil, fmt.Errorf("hosts without a driver do not support SSH") +} diff --git a/drivers/utils.go b/drivers/utils.go new file mode 100644 index 0000000000..0fa57f10c8 --- /dev/null +++ b/drivers/utils.go @@ -0,0 +1,23 @@ +package drivers + +import ( + "fmt" + "os" + "path/filepath" +) + +func AddPublicKeyToAuthorizedHosts(d Driver, authorizedKeysPath string) error { + f, err := os.Open(filepath.Join(os.Getenv("HOME"), ".docker/public-key.json")) + if err != nil { + return err + } + defer f.Close() + + cmdString := fmt.Sprintf("mkdir -p %q && cat > %q", authorizedKeysPath, filepath.Join(authorizedKeysPath, "docker-host.json")) + cmd, err := d.GetSSHCommand(cmdString) + if err != nil { + return err + } + cmd.Stdin = f + return cmd.Run() +} diff --git a/drivers/virtualbox/network.go b/drivers/virtualbox/network.go new file mode 100644 index 0000000000..95f569a353 --- /dev/null +++ b/drivers/virtualbox/network.go @@ -0,0 +1,264 @@ +package virtualbox + +import ( + "bufio" + "errors" + "fmt" + "net" + "regexp" + "strconv" + "strings" +) + +var ( + reHostonlyInterfaceCreated = regexp.MustCompile(`Interface '(.+)' was successfully created`) +) + +// Host-only network. +type hostOnlyNetwork struct { + Name string + GUID string + DHCP bool + IPv4 net.IPNet + IPv6 net.IPNet + HwAddr net.HardwareAddr + Medium string + Status string + NetworkName string // referenced in DHCP.NetworkName +} + +// Config changes the configuration of the host-only network. +func (n *hostOnlyNetwork) Save() error { + if n.IPv4.IP != nil && n.IPv4.Mask != nil { + if err := vbm("hostonlyif", "ipconfig", n.Name, "--ip", n.IPv4.IP.String(), "--netmask", net.IP(n.IPv4.Mask).String()); err != nil { + return err + } + } + + if n.IPv6.IP != nil && n.IPv6.Mask != nil { + prefixLen, _ := n.IPv6.Mask.Size() + if err := vbm("hostonlyif", "ipconfig", n.Name, "--ipv6", n.IPv6.IP.String(), "--netmasklengthv6", fmt.Sprintf("%d", prefixLen)); err != nil { + return err + } + } + + if n.DHCP { + vbm("hostonlyif", "ipconfig", n.Name, "--dhcp") // not implemented as of VirtualBox 4.3 + } + + return nil +} + +// createHostonlyNet creates a new host-only network. +func createHostonlyNet() (*hostOnlyNetwork, error) { + out, err := vbmOut("hostonlyif", "create") + if err != nil { + return nil, err + } + res := reHostonlyInterfaceCreated.FindStringSubmatch(string(out)) + if res == nil { + return nil, errors.New("failed to create hostonly interface") + } + return &hostOnlyNetwork{Name: res[1]}, nil +} + +// HostonlyNets gets all host-only networks in a map keyed by HostonlyNet.NetworkName. +func listHostOnlyNetworks() (map[string]*hostOnlyNetwork, error) { + out, err := vbmOut("list", "hostonlyifs") + if err != nil { + return nil, err + } + s := bufio.NewScanner(strings.NewReader(out)) + m := map[string]*hostOnlyNetwork{} + n := &hostOnlyNetwork{} + for s.Scan() { + line := s.Text() + if line == "" { + m[n.NetworkName] = n + n = &hostOnlyNetwork{} + continue + } + res := reColonLine.FindStringSubmatch(line) + if res == nil { + continue + } + switch key, val := res[1], res[2]; key { + case "Name": + n.Name = val + case "GUID": + n.GUID = val + case "DHCP": + n.DHCP = (val != "Disabled") + case "IPAddress": + n.IPv4.IP = net.ParseIP(val) + case "NetworkMask": + n.IPv4.Mask = parseIPv4Mask(val) + case "IPV6Address": + n.IPv6.IP = net.ParseIP(val) + case "IPV6NetworkMaskPrefixLength": + l, err := strconv.ParseUint(val, 10, 7) + if err != nil { + return nil, err + } + n.IPv6.Mask = net.CIDRMask(int(l), net.IPv6len*8) + case "HardwareAddress": + mac, err := net.ParseMAC(val) + if err != nil { + return nil, err + } + n.HwAddr = mac + case "MediumType": + n.Medium = val + case "Status": + n.Status = val + case "VBoxNetworkName": + n.NetworkName = val + } + } + if err := s.Err(); err != nil { + return nil, err + } + return m, nil +} + +func getHostOnlyNetwork(hostIP net.IP, netmask net.IPMask) (*hostOnlyNetwork, error) { + nets, err := listHostOnlyNetworks() + if err != nil { + return nil, err + } + for _, n := range nets { + if hostIP.Equal(n.IPv4.IP) && + netmask.String() == n.IPv4.Mask.String() { + return n, nil + } + } + return nil, nil +} + +func getOrCreateHostOnlyNetwork(hostIP net.IP, netmask net.IPMask, dhcpIP net.IP, dhcpUpperIP net.IP, dhcpLowerIP net.IP) (*hostOnlyNetwork, error) { + hostOnlyNet, err := getHostOnlyNetwork(hostIP, netmask) + if err != nil || hostOnlyNet != nil { + return hostOnlyNet, err + } + // No existing host-only interface found. Create a new one. + hostOnlyNet, err = createHostonlyNet() + if err != nil { + return nil, err + } + hostOnlyNet.IPv4.IP = hostIP + hostOnlyNet.IPv4.Mask = netmask + if err := hostOnlyNet.Save(); err != nil { + return nil, err + } + + dhcp := dhcpServer{} + dhcp.IPv4.IP = dhcpIP + dhcp.IPv4.Mask = netmask + dhcp.LowerIP = dhcpUpperIP + dhcp.UpperIP = dhcpLowerIP + dhcp.Enabled = true + if err := addHostonlyDHCP(hostOnlyNet.Name, dhcp); err != nil { + return nil, err + } + + return hostOnlyNet, nil +} + +// DHCP server info. +type dhcpServer struct { + NetworkName string + IPv4 net.IPNet + LowerIP net.IP + UpperIP net.IP + Enabled bool +} + +func addDHCPServer(kind, name string, d dhcpServer) error { + command := "modify" + + // On some platforms (OSX), creating a hostonlyinterface adds a default dhcpserver + // While on others (Windows?) it does not. + dhcps, err := getDHCPServers() + if err != nil { + return err + } + + if _, ok := dhcps[name]; !ok { + command = "add" + } + + args := []string{"dhcpserver", command, + kind, name, + "--ip", d.IPv4.IP.String(), + "--netmask", net.IP(d.IPv4.Mask).String(), + "--lowerip", d.LowerIP.String(), + "--upperip", d.UpperIP.String(), + } + if d.Enabled { + args = append(args, "--enable") + } else { + args = append(args, "--disable") + } + return vbm(args...) +} + +// AddInternalDHCP adds a DHCP server to an internal network. +func addInternalDHCP(netname string, d dhcpServer) error { + return addDHCPServer("--netname", netname, d) +} + +// AddHostonlyDHCP adds a DHCP server to a host-only network. +func addHostonlyDHCP(ifname string, d dhcpServer) error { + return addDHCPServer("--netname", "HostInterfaceNetworking-"+ifname, d) +} + +// DHCPs gets all DHCP server settings in a map keyed by DHCP.NetworkName. +func getDHCPServers() (map[string]*dhcpServer, error) { + out, err := vbmOut("list", "dhcpservers") + if err != nil { + return nil, err + } + s := bufio.NewScanner(strings.NewReader(out)) + m := map[string]*dhcpServer{} + dhcp := &dhcpServer{} + for s.Scan() { + line := s.Text() + if line == "" { + m[dhcp.NetworkName] = dhcp + dhcp = &dhcpServer{} + continue + } + res := reColonLine.FindStringSubmatch(line) + if res == nil { + continue + } + switch key, val := res[1], res[2]; key { + case "NetworkName": + dhcp.NetworkName = val + case "IP": + dhcp.IPv4.IP = net.ParseIP(val) + case "upperIPAddress": + dhcp.UpperIP = net.ParseIP(val) + case "lowerIPAddress": + dhcp.LowerIP = net.ParseIP(val) + case "NetworkMask": + dhcp.IPv4.Mask = parseIPv4Mask(val) + case "Enabled": + dhcp.Enabled = (val == "Yes") + } + } + if err := s.Err(); err != nil { + return nil, err + } + return m, nil +} + +// parseIPv4Mask parses IPv4 netmask written in IP form (e.g. 255.255.255.0). +// This function should really belong to the net package. +func parseIPv4Mask(s string) net.IPMask { + mask := net.ParseIP(s) + if mask == nil { + return nil + } + return net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]) +} diff --git a/drivers/virtualbox/vbm.go b/drivers/virtualbox/vbm.go new file mode 100644 index 0000000000..1e86096229 --- /dev/null +++ b/drivers/virtualbox/vbm.go @@ -0,0 +1,75 @@ +package virtualbox + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "regexp" + "strings" + + log "github.com/Sirupsen/logrus" +) + +var ( + reVMNameUUID = regexp.MustCompile(`"(.+)" {([0-9a-f-]+)}`) + reVMInfoLine = regexp.MustCompile(`(?:"(.+)"|(.+))=(?:"(.*)"|(.*))`) + reColonLine = regexp.MustCompile(`(.+):\s+(.*)`) + reMachineNotFound = regexp.MustCompile(`Could not find a registered machine named '(.+)'`) +) + +var ( + ErrMachineExist = errors.New("machine already exists") + ErrMachineNotExist = errors.New("machine does not exist") + ErrVBMNotFound = errors.New("VBoxManage not found") + vboxManageCmd = "VBoxManage" +) + +func vbm(args ...string) error { + cmd := exec.Command(vboxManageCmd, args...) + if os.Getenv("DEBUG") != "" { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + log.Debugf("executing: %v %v", vboxManageCmd, strings.Join(args, " ")) + if err := cmd.Run(); err != nil { + if ee, ok := err.(*exec.Error); ok && ee == exec.ErrNotFound { + return ErrVBMNotFound + } + return fmt.Errorf("%v %v failed: %v", vboxManageCmd, strings.Join(args, " "), err) + } + return nil +} + +func vbmOut(args ...string) (string, error) { + cmd := exec.Command(vboxManageCmd, args...) + if os.Getenv("DEBUG") != "" { + cmd.Stderr = os.Stderr + } + log.Debugf("executing: %v %v", vboxManageCmd, strings.Join(args, " ")) + + b, err := cmd.Output() + if err != nil { + if ee, ok := err.(*exec.Error); ok && ee == exec.ErrNotFound { + err = ErrVBMNotFound + } + } + return string(b), err +} + +func vbmOutErr(args ...string) (string, string, error) { + cmd := exec.Command(vboxManageCmd, args...) + log.Debugf("executing: %v %v", vboxManageCmd, strings.Join(args, " ")) + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err != nil { + if ee, ok := err.(*exec.Error); ok && ee == exec.ErrNotFound { + err = ErrVBMNotFound + } + } + return stdout.String(), stderr.String(), err +} diff --git a/drivers/virtualbox/virtualbox.go b/drivers/virtualbox/virtualbox.go new file mode 100644 index 0000000000..f62a468dab --- /dev/null +++ b/drivers/virtualbox/virtualbox.go @@ -0,0 +1,625 @@ +package virtualbox + +import ( + "archive/tar" + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "os" + "os/exec" + "path" + "regexp" + "runtime" + "strconv" + "strings" + "time" + + log "github.com/Sirupsen/logrus" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/utils" + "github.com/docker/machine/drivers" + "github.com/docker/machine/ssh" + "github.com/docker/machine/state" +) + +type Driver struct { + MachineName string + SSHPort int + Memory int + DiskSize int + Boot2DockerURL string + storePath string +} + +type CreateFlags struct { + Memory *int + DiskSize *int + Boot2DockerURL *string +} + +func init() { + drivers.Register("virtualbox", &drivers.RegisteredDriver{ + New: NewDriver, + RegisterCreateFlags: RegisterCreateFlags, + }) +} + +// RegisterCreateFlags registers the flags this driver adds to +// "docker hosts create" +func RegisterCreateFlags(cmd *flag.FlagSet) interface{} { + createFlags := new(CreateFlags) + createFlags.Memory = cmd.Int([]string{"-virtualbox-memory"}, 1024, "Size of memory for host in MB") + createFlags.DiskSize = cmd.Int([]string{"-virtualbox-disk-size"}, 20000, "Size of disk for host in MB") + createFlags.Boot2DockerURL = cmd.String([]string{"-virtualbox-boot2docker-url"}, "", "The URL of the boot2docker image. Defaults to the latest available version") + return createFlags +} + +func NewDriver(storePath string) (drivers.Driver, error) { + return &Driver{storePath: storePath}, nil +} + +func (d *Driver) DriverName() string { + return "virtualbox" +} + +func (d *Driver) GetURL() (string, error) { + ip, err := d.GetIP() + if err != nil { + return "", err + } + if ip == "" { + return "", nil + } + return fmt.Sprintf("tcp://%s:2376", ip), nil +} + +func (d *Driver) SetConfigFromFlags(flagsInterface interface{}) error { + flags := flagsInterface.(*CreateFlags) + d.Memory = *flags.Memory + d.DiskSize = *flags.DiskSize + d.Boot2DockerURL = *flags.Boot2DockerURL + return nil +} + +func (d *Driver) Create() error { + var ( + err error + isoURL string + ) + + d.SSHPort, err = getAvailableTCPPort() + if err != nil { + return err + } + d.setMachineNameIfNotSet() + + // HACK: if ~/.docker/boot2docker.iso exists, use that + localISOPath := path.Join(os.Getenv("HOME"), ".docker/boot2docker.iso") + if _, err := os.Stat(localISOPath); err == nil { + cmd := exec.Command("cp", localISOPath, path.Join(d.storePath, "boot2docker.iso")) + if err := cmd.Run(); err != nil { + return err + } + } else { + if d.Boot2DockerURL != "" { + isoURL = d.Boot2DockerURL + } else { + // HACK: Docker 1.3 boot2docker image with client/daemon auth + isoURL = "https://bfirsh.s3.amazonaws.com/boot2docker/boot2docker-1.3.1-identity-auth.iso" + // isoURL, err = getLatestReleaseURL() + // if err != nil { + // return err + // } + } + log.Infof("Downloading boot2docker...") + if err := downloadISO(d.storePath, "boot2docker.iso", isoURL); err != nil { + return err + } + } + + log.Infof("Creating SSH key...") + + if err := ssh.GenerateSSHKey(d.sshKeyPath()); err != nil { + return err + } + + log.Infof("Creating VirtualBox VM...") + + if err := d.generateDiskImage(d.DiskSize); err != nil { + return err + } + + if err := vbm("createvm", + "--name", d.MachineName, + "--register"); err != nil { + return err + } + + cpus := uint(runtime.NumCPU()) + if cpus > 32 { + cpus = 32 + } + + if err := vbm("modifyvm", d.MachineName, + "--firmware", "bios", + "--bioslogofadein", "off", + "--bioslogofadeout", "off", + "--natdnshostresolver1", "on", + "--bioslogodisplaytime", "0", + "--biosbootmenu", "disabled", + + "--ostype", "Linux26_64", + "--cpus", fmt.Sprintf("%d", cpus), + "--memory", fmt.Sprintf("%d", d.Memory), + + "--acpi", "on", + "--ioapic", "on", + "--rtcuseutc", "on", + "--cpuhotplug", "off", + "--pae", "on", + "--synthcpu", "off", + "--hpet", "on", + "--hwvirtex", "on", + "--nestedpaging", "on", + "--largepages", "on", + "--vtxvpid", "on", + "--accelerate3d", "off", + "--boot1", "dvd"); err != nil { + return err + } + + if err := vbm("modifyvm", d.MachineName, + "--nic1", "nat", + "--nictype1", "virtio", + "--cableconnected1", "on"); err != nil { + return err + } + + if err := vbm("modifyvm", d.MachineName, + "--natpf1", fmt.Sprintf("ssh,tcp,127.0.0.1,%d,,22", d.SSHPort)); err != nil { + return err + } + + hostOnlyNetwork, err := getOrCreateHostOnlyNetwork( + net.ParseIP("192.168.99.1"), + net.IPv4Mask(255, 255, 255, 0), + net.ParseIP("192.168.99.2"), + net.ParseIP("192.168.99.100"), + net.ParseIP("192.168.99.254")) + if err != nil { + return err + } + if err := vbm("modifyvm", d.MachineName, + "--nic2", "hostonly", + "--nictype2", "virtio", + "--hostonlyadapter2", hostOnlyNetwork.Name, + "--cableconnected2", "on"); err != nil { + return err + } + + if err := vbm("storagectl", d.MachineName, + "--name", "SATA", + "--add", "sata", + "--hostiocache", "on"); err != nil { + return err + } + + if err := vbm("storageattach", d.MachineName, + "--storagectl", "SATA", + "--port", "0", + "--device", "0", + "--type", "dvddrive", + "--medium", path.Join(d.storePath, "boot2docker.iso")); err != nil { + return err + } + + if err := vbm("storageattach", d.MachineName, + "--storagectl", "SATA", + "--port", "1", + "--device", "0", + "--type", "hdd", + "--medium", d.diskPath()); err != nil { + return err + } + + // let VBoxService do nice magic automounting (when it's used) + if err := vbm("guestproperty", "set", d.MachineName, "/VirtualBox/GuestAdd/SharedFolders/MountPrefix", "/"); err != nil { + return err + } + if err := vbm("guestproperty", "set", d.MachineName, "/VirtualBox/GuestAdd/SharedFolders/MountDir", "/"); err != nil { + return err + } + + var shareName, shareDir string // TODO configurable at some point + switch runtime.GOOS { + case "darwin": + shareName = "Users" + shareDir = "/Users" + // TODO "linux" and "windows" + } + + if shareDir != "" { + if _, err := os.Stat(shareDir); err != nil && !os.IsNotExist(err) { + return err + } else if !os.IsNotExist(err) { + if shareName == "" { + // parts of the VBox internal code are buggy with share names that start with "/" + shareName = strings.TrimLeft(shareDir, "/") + // TODO do some basic Windows -> MSYS path conversion + // ie, s!^([a-z]+):[/\\]+!\1/!; s!\\!/!g + } + + // woo, shareDir exists! let's carry on! + if err := vbm("sharedfolder", "add", d.MachineName, "--name", shareName, "--hostpath", shareDir, "--automount"); err != nil { + return err + } + + // enable symlinks + if err := vbm("setextradata", d.MachineName, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/"+shareName, "1"); err != nil { + return err + } + } + } + + log.Infof("Starting VirtualBox VM...") + + if err := d.Start(); err != nil { + return err + } + + log.Debugf("Adding key to authorized-keys.d...") + + if err := drivers.AddPublicKeyToAuthorizedHosts(d, "/root/.docker/authorized-keys.d"); err != nil { + return err + } + + cmd, err := d.GetSSHCommand("sudo /etc/init.d/docker restart") + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + + return nil +} + +func (d *Driver) Start() error { + if err := vbm("startvm", d.MachineName, "--type", "headless"); err != nil { + return err + } + log.Infof("Waiting for VM to start...") + return ssh.WaitForTCP(fmt.Sprintf("localhost:%d", d.SSHPort)) +} + +func (d *Driver) Stop() error { + if err := vbm("controlvm", d.MachineName, "acpipowerbutton"); err != nil { + return err + } + for { + s, err := d.GetState() + if err != nil { + return err + } + if s == state.Running { + time.Sleep(1 * time.Second) + } else { + break + } + } + return nil +} + +func (d *Driver) Remove() error { + s, err := d.GetState() + if err != nil { + if err == ErrMachineNotExist { + log.Infof("machine does not exist, assuming it has been removed already") + return nil + } + return err + } + if s == state.Running { + if err := d.Kill(); err != nil { + return err + } + } + return vbm("unregistervm", "--delete", d.MachineName) +} + +func (d *Driver) Restart() error { + if err := d.Stop(); err != nil { + return err + } + return d.Start() +} + +func (d *Driver) Kill() error { + return vbm("controlvm", d.MachineName, "poweroff") +} + +func (d *Driver) Upgrade() error { + log.Infof("Stopping machine...") + if err := d.Stop(); err != nil { + return err + } + + isoURL, err := getLatestReleaseURL() + if err != nil { + return err + } + + log.Infof("Downloading boot2docker...") + if err := downloadISO(d.storePath, "boot2docker.iso", isoURL); err != nil { + return err + } + + log.Infof("Starting machine...") + if err := d.Start(); err != nil { + return err + } + + return nil +} + +func (d *Driver) GetState() (state.State, error) { + stdout, stderr, err := vbmOutErr("showvminfo", d.MachineName, + "--machinereadable") + if err != nil { + if reMachineNotFound.FindString(stderr) != "" { + return state.Error, ErrMachineNotExist + } + return state.Error, err + } + re := regexp.MustCompile(`(?m)^VMState="(\w+)"$`) + groups := re.FindStringSubmatch(stdout) + if len(groups) < 1 { + return state.None, nil + } + switch groups[1] { + case "running": + return state.Running, nil + case "paused": + return state.Paused, nil + case "saved": + return state.Saved, nil + case "poweroff", "aborted": + return state.Stopped, nil + } + return state.None, nil +} + +func (d *Driver) setMachineNameIfNotSet() { + if d.MachineName == "" { + d.MachineName = fmt.Sprintf("docker-host-%s", utils.GenerateRandomID()) + } +} + +func (d *Driver) GetIP() (string, error) { + // DHCP is used to get the IP, so virtualbox hosts don't have IPs unless + // they are running + s, err := d.GetState() + if err != nil { + return "", err + } + if s != state.Running { + return "", drivers.ErrHostIsNotRunning + } + + cmd, err := d.GetSSHCommand("ip addr show dev eth1") + if err != nil { + return "", err + } + + b, err := cmd.Output() + if err != nil { + return "", err + } + out := string(b) + log.Debugf("SSH returned: %s\nEND SSH\n", out) + // parse to find: inet 192.168.59.103/24 brd 192.168.59.255 scope global eth1 + lines := strings.Split(out, "\n") + for _, line := range lines { + vals := strings.Split(strings.TrimSpace(line), " ") + if len(vals) >= 2 && vals[0] == "inet" { + return vals[1][:strings.Index(vals[1], "/")], nil + } + } + + return "", fmt.Errorf("No IP address found %s", out) +} + +func (d *Driver) GetSSHCommand(args ...string) (*exec.Cmd, error) { + return ssh.GetSSHCommand("localhost", d.SSHPort, "docker", d.sshKeyPath(), args...), nil +} + +func (d *Driver) sshKeyPath() string { + return path.Join(d.storePath, "id_rsa") +} + +func (d *Driver) publicSSHKeyPath() string { + return d.sshKeyPath() + ".pub" +} + +func (d *Driver) diskPath() string { + return path.Join(d.storePath, "disk.vmdk") +} + +// Get the latest boot2docker release tag name (e.g. "v0.6.0"). +// FIXME: find or create some other way to get the "latest release" of boot2docker since the GitHub API has a pretty low rate limit on API requests +func getLatestReleaseURL() (string, error) { + rsp, err := http.Get("https://api.github.com/repos/boot2docker/boot2docker/releases") + if err != nil { + return "", err + } + defer rsp.Body.Close() + + var t []struct { + TagName string `json:"tag_name"` + } + if err := json.NewDecoder(rsp.Body).Decode(&t); err != nil { + return "", err + } + if len(t) == 0 { + return "", fmt.Errorf("no releases found") + } + + tag := t[0].TagName + url := fmt.Sprintf("https://github.com/boot2docker/boot2docker/releases/download/%s/boot2docker.iso", tag) + return url, nil +} + +// Download boot2docker ISO image for the given tag and save it at dest. +func downloadISO(dir, file, url string) error { + rsp, err := http.Get(url) + if err != nil { + return err + } + defer rsp.Body.Close() + + // Download to a temp file first then rename it to avoid partial download. + f, err := ioutil.TempFile(dir, file+".tmp") + if err != nil { + return err + } + defer os.Remove(f.Name()) + if _, err := io.Copy(f, rsp.Body); err != nil { + // TODO: display download progress? + return err + } + if err := f.Close(); err != nil { + return err + } + if err := os.Rename(f.Name(), path.Join(dir, file)); err != nil { + return err + } + return nil +} + +// Make a boot2docker VM disk image. +func (d *Driver) generateDiskImage(size int) error { + log.Debugf("Creating %d MB hard disk image...", size) + + magicString := "boot2docker, please format-me" + + buf := new(bytes.Buffer) + tw := tar.NewWriter(buf) + + // magicString first so the automount script knows to format the disk + file := &tar.Header{Name: magicString, Size: int64(len(magicString))} + if err := tw.WriteHeader(file); err != nil { + return err + } + if _, err := tw.Write([]byte(magicString)); err != nil { + return err + } + // .ssh/key.pub => authorized_keys + file = &tar.Header{Name: ".ssh", Typeflag: tar.TypeDir, Mode: 0700} + if err := tw.WriteHeader(file); err != nil { + return err + } + pubKey, err := ioutil.ReadFile(d.publicSSHKeyPath()) + if err != nil { + return err + } + file = &tar.Header{Name: ".ssh/authorized_keys", Size: int64(len(pubKey)), Mode: 0644} + if err := tw.WriteHeader(file); err != nil { + return err + } + if _, err := tw.Write([]byte(pubKey)); err != nil { + return err + } + file = &tar.Header{Name: ".ssh/authorized_keys2", Size: int64(len(pubKey)), Mode: 0644} + if err := tw.WriteHeader(file); err != nil { + return err + } + if _, err := tw.Write([]byte(pubKey)); err != nil { + return err + } + if err := tw.Close(); err != nil { + return err + } + raw := bytes.NewReader(buf.Bytes()) + return createDiskImage(d.diskPath(), size, raw) +} + +// createDiskImage makes a disk image at dest with the given size in MB. If r is +// not nil, it will be read as a raw disk image to convert from. +func createDiskImage(dest string, size int, r io.Reader) error { + // Convert a raw image from stdin to the dest VMDK image. + sizeBytes := int64(size) << 20 // usually won't fit in 32-bit int (max 2GB) + // FIXME: why isn't this just using the vbm*() functions? + cmd := exec.Command(vboxManageCmd, "convertfromraw", "stdin", dest, + fmt.Sprintf("%d", sizeBytes), "--format", "VMDK") + + if os.Getenv("DEBUG") != "" { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + + stdin, err := cmd.StdinPipe() + if err != nil { + return err + } + if err := cmd.Start(); err != nil { + return err + } + + n, err := io.Copy(stdin, r) + if err != nil { + return err + } + + // The total number of bytes written to stdin must match sizeBytes, or + // VBoxManage.exe on Windows will fail. Fill remaining with zeros. + if left := sizeBytes - n; left > 0 { + if err := zeroFill(stdin, left); err != nil { + return err + } + } + + // cmd won't exit until the stdin is closed. + if err := stdin.Close(); err != nil { + return err + } + + return cmd.Wait() +} + +// zeroFill writes n zero bytes into w. +func zeroFill(w io.Writer, n int64) error { + const blocksize = 32 << 10 + zeros := make([]byte, blocksize) + var k int + var err error + for n > 0 { + if n > blocksize { + k, err = w.Write(zeros) + } else { + k, err = w.Write(zeros[:n]) + } + if err != nil { + return err + } + n -= int64(k) + } + return nil +} + +func getAvailableTCPPort() (int, error) { + // FIXME: this has a race condition between finding an available port and + // virtualbox using that port. Perhaps we should randomly pick an unused + // port in a range not used by kernel for assigning ports + ln, err := net.Listen("tcp4", "127.0.0.1:0") + if err != nil { + return 0, err + } + defer ln.Close() + addr := ln.Addr().String() + addrParts := strings.SplitN(addr, ":", 2) + return strconv.Atoi(addrParts[1]) +} diff --git a/host.go b/host.go new file mode 100644 index 0000000000..aa3476034b --- /dev/null +++ b/host.go @@ -0,0 +1,142 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path" + "regexp" + + log "github.com/Sirupsen/logrus" + "github.com/docker/machine/drivers" +) + +var ( + validHostNameChars = `[a-zA-Z0-9_]` + validHostNamePattern = regexp.MustCompile(`^` + validHostNameChars + `+$`) +) + +type Host struct { + Name string `json:"-"` + DriverName string + Driver drivers.Driver + storePath string +} + +type hostConfig struct { + DriverName string +} + +func NewHost(name, driverName, storePath string) (*Host, error) { + driver, err := drivers.NewDriver(driverName, storePath) + if err != nil { + return nil, err + } + return &Host{ + Name: name, + DriverName: driverName, + Driver: driver, + storePath: storePath, + }, nil +} + +func LoadHost(name string, storePath string) (*Host, error) { + if _, err := os.Stat(storePath); os.IsNotExist(err) { + return nil, fmt.Errorf("Host %q does not exist", name) + } + + host := &Host{Name: name, storePath: storePath} + if err := host.LoadConfig(); err != nil { + return nil, err + } + return host, nil +} + +func ValidateHostName(name string) (string, error) { + if !validHostNamePattern.MatchString(name) { + return name, fmt.Errorf("Invalid host name %q, it must match %s", name, validHostNamePattern) + } + return name, nil +} + +func (h *Host) Create() error { + if err := h.Driver.Create(); err != nil { + return err + } + if err := h.SaveConfig(); err != nil { + return err + } + return nil +} + +func (h *Host) Start() error { + return h.Driver.Start() +} + +func (h *Host) Stop() error { + return h.Driver.Stop() +} + +func (h *Host) Remove(force bool) error { + if err := h.Driver.Remove(); err != nil { + if force { + log.Errorf("Error removing host, force removing anyway: %s", err) + } else { + return err + } + } + return h.removeStorePath() +} + +func (h *Host) removeStorePath() error { + file, err := os.Stat(h.storePath) + if err != nil { + return err + } + if !file.IsDir() { + return fmt.Errorf("%q is not a directory", h.storePath) + } + return os.RemoveAll(h.storePath) +} + +func (h *Host) GetURL() (string, error) { + return h.Driver.GetURL() +} + +func (h *Host) LoadConfig() error { + data, err := ioutil.ReadFile(path.Join(h.storePath, "config.json")) + if err != nil { + return err + } + + // First pass: find the driver name and load the driver + var config hostConfig + if err := json.Unmarshal(data, &config); err != nil { + return err + } + + driver, err := drivers.NewDriver(config.DriverName, h.storePath) + if err != nil { + return err + } + h.Driver = driver + + // Second pass: unmarshal driver config into correct driver + if err := json.Unmarshal(data, &h); err != nil { + return err + } + + return nil +} + +func (h *Host) SaveConfig() error { + data, err := json.Marshal(h) + if err != nil { + return err + } + if err := ioutil.WriteFile(path.Join(h.storePath, "config.json"), data, 0600); err != nil { + return err + } + return nil +} diff --git a/log.go b/log.go new file mode 100644 index 0000000000..cdbbd4408f --- /dev/null +++ b/log.go @@ -0,0 +1,12 @@ +package main + +import ( + "os" + + log "github.com/Sirupsen/logrus" +) + +func initLogging(lvl log.Level) { + log.SetOutput(os.Stderr) + log.SetLevel(lvl) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000000..977fb1f042 --- /dev/null +++ b/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + "os" + + log "github.com/Sirupsen/logrus" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/utils" +) + +var ( + flVersion = flag.Bool([]string{"v", "-version"}, false, "Print version information and quit") + flDebug = flag.Bool([]string{"D", "-debug"}, false, "Enable debug mode") +) + +func main() { + flag.Usage = func() { + fmt.Fprint(os.Stderr, "Usage: machine [OPTIONS] COMMAND [arg...]\n\nCreate and manage machines running Docker.\n\nOptions:\n") + + flag.PrintDefaults() + + help := "\nCommands:\n" + + for _, command := range [][]string{ + {"active", "Get or set the active machine"}, + {"create", "Create a machine"}, + {"inspect", "Inspect information about a machine"}, + {"ip", "Get the IP address of a machine"}, + {"kill", "Kill a machine"}, + {"ls", "List machines"}, + {"restart", "Restart a machine"}, + {"rm", "Remove a machine"}, + {"ssh", "Log into or run a command on a machine with SSH"}, + {"start", "Start a machine"}, + {"stop", "Stop a machine"}, + {"upgrade", "Upgrade a machine to the latest version of Docker"}, + {"url", "Get the URL of a machine"}, + } { + help += fmt.Sprintf(" %-10.10s%s\n", command[0], command[1]) + } + help += "\nRun 'docker COMMAND --help' for more information on a command." + fmt.Fprintf(os.Stderr, "%s\n", help) + } + + flag.Parse() + + // -D, --debug, -l/--log-level=debug processing + // When/if -D is removed this block can be deleted + if *flDebug { + os.Setenv("DEBUG", "1") + initLogging(log.DebugLevel) + } + + cli := &DockerCli{} + + if err := cli.Cmd(flag.Args()...); err != nil { + if sterr, ok := err.(*utils.StatusError); ok { + if sterr.Status != "" { + log.Println(sterr.Status) + } + os.Exit(sterr.StatusCode) + } + log.Fatal(err) + } +} diff --git a/ssh/ssh.go b/ssh/ssh.go new file mode 100644 index 0000000000..383532e1ae --- /dev/null +++ b/ssh/ssh.go @@ -0,0 +1,73 @@ +package ssh + +import ( + "fmt" + "net" + "os" + "os/exec" + "strings" + + log "github.com/Sirupsen/logrus" +) + +func GetSSHCommand(host string, port int, user string, sshKey string, args ...string) *exec.Cmd { + + defaultSSHArgs := []string{ + "-o", "IdentitiesOnly=yes", + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + "-o", "LogLevel=quiet", // suppress "Warning: Permanently added '[localhost]:2022' (ECDSA) to the list of known hosts." + "-p", fmt.Sprintf("%d", port), + "-i", sshKey, + fmt.Sprintf("%s@%s", user, host), + } + + sshArgs := append(defaultSSHArgs, args...) + cmd := exec.Command("ssh", sshArgs...) + cmd.Stderr = os.Stderr + + if os.Getenv("DEBUG") != "" { + cmd.Stdout = os.Stdout + } + + log.Debugf("executing: %v", strings.Join(cmd.Args, " ")) + + return cmd +} + +func GenerateSSHKey(path string) error { + if _, err := os.Stat(path); err != nil { + if !os.IsNotExist(err) { + return err + } + + cmd := exec.Command("ssh-keygen", "-t", "rsa", "-N", "", "-f", path) + + if os.Getenv("DEBUG") != "" { + cmd.Stdout = os.Stdout + } + + cmd.Stderr = os.Stderr + log.Debugf("executing: %v %v\n", cmd.Path, strings.Join(cmd.Args, " ")) + + if err := cmd.Run(); err != nil { + return err + } + } + return nil +} + +func WaitForTCP(addr string) error { + for { + conn, err := net.Dial("tcp", addr) + if err != nil { + continue + } + defer conn.Close() + if _, err = conn.Read(make([]byte, 1)); err != nil { + continue + } + break + } + return nil +} diff --git a/state/state.go b/state/state.go new file mode 100644 index 0000000000..9c7e8275e4 --- /dev/null +++ b/state/state.go @@ -0,0 +1,31 @@ +package state + +// State represents the state of a hosts +type State int + +const ( + None State = iota + Running + Paused + Saved + Stopped + Starting + Error +) + +var states = []string{ + "", + "Running", + "Paused", + "Saved", + "Stopped", + "Starting", + "Error", +} + +func (s State) String() string { + if int(s) < len(states) { + return states[s] + } + return "" +} diff --git a/state/state_test.go b/state/state_test.go new file mode 100644 index 0000000000..b53bb70e33 --- /dev/null +++ b/state/state_test.go @@ -0,0 +1,17 @@ +package state + +import ( + "testing" +) + +func TestDaemonCreate(t *testing.T) { + if None.String() != "" { + t.Fatal("None state should be empty string") + } + if Running.String() != "Running" { + t.Fatal("Running state should be 'Running'") + } + if Error.String() != "Error" { + t.Fatal("Error state should be 'Error'") + } +} diff --git a/store.go b/store.go new file mode 100644 index 0000000000..0b66925c26 --- /dev/null +++ b/store.go @@ -0,0 +1,152 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path" + + log "github.com/Sirupsen/logrus" + homedir "github.com/mitchellh/go-homedir" +) + +// Store persists hosts on the filesystem +type Store struct { + Path string +} + +func NewStore() *Store { + homeDir, err := homedir.Dir() + if err != nil { + log.Errorf("error getting home directory : %s", err) + } + rootPath := path.Join(homeDir, ".docker/hosts") + return &Store{Path: rootPath} +} + +func (s *Store) Create(name string, driverName string, createFlags interface{}) (*Host, error) { + exists, err := s.Exists(name) + if err != nil { + return nil, err + } + if exists { + return nil, fmt.Errorf("Host %q already exists", name) + } + + hostPath := path.Join(s.Path, name) + + host, err := NewHost(name, driverName, hostPath) + if err != nil { + return host, err + } + if createFlags != nil { + if err := host.Driver.SetConfigFromFlags(createFlags); err != nil { + return host, err + } + } + + if err := os.MkdirAll(hostPath, 0700); err != nil { + return nil, err + } + + if err := host.SaveConfig(); err != nil { + return host, err + } + + if err := host.Create(); err != nil { + return host, err + } + return host, nil +} + +func (s *Store) Remove(name string, force bool) error { + active, err := s.GetActive() + if err != nil { + return err + } + if active != nil && active.Name == name { + if err := s.RemoveActive(); err != nil { + return err + } + } + + host, err := s.Load(name) + if err != nil { + return err + } + return host.Remove(force) +} + +func (s *Store) List() ([]Host, error) { + dir, err := ioutil.ReadDir(s.Path) + if err != nil && !os.IsNotExist(err) { + return nil, err + } + + hosts := []Host{} + + for _, file := range dir { + if file.IsDir() { + host, err := s.Load(file.Name()) + if err != nil { + log.Errorf("error loading host %q: %s", file.Name(), err) + continue + } + hosts = append(hosts, *host) + } + } + return hosts, nil +} + +func (s *Store) Exists(name string) (bool, error) { + _, err := os.Stat(path.Join(s.Path, name)) + if os.IsNotExist(err) { + return false, nil + } else if err == nil { + return true, nil + } + return false, err +} + +func (s *Store) Load(name string) (*Host, error) { + hostPath := path.Join(s.Path, name) + return LoadHost(name, hostPath) +} + +func (s *Store) GetActive() (*Host, error) { + hostName, err := ioutil.ReadFile(s.activePath()) + if os.IsNotExist(err) { + return nil, nil + } else if err != nil { + return nil, err + } + return s.Load(string(hostName)) +} + +func (s *Store) IsActive(host *Host) (bool, error) { + active, err := s.GetActive() + if err != nil { + return false, err + } + if active == nil { + return false, nil + } + return active.Name == host.Name, nil +} + +func (s *Store) SetActive(host *Host) error { + if err := os.MkdirAll(path.Dir(s.activePath()), 0700); err != nil { + return err + } + return ioutil.WriteFile(s.activePath(), []byte(host.Name), 0600) +} + +func (s *Store) RemoveActive() error { + return os.Remove(s.activePath()) +} + +// activePath returns the path to the file that stores the name of the +// active host +func (s *Store) activePath() string { + return path.Join(s.Path, ".active") +} diff --git a/store_test.go b/store_test.go new file mode 100644 index 0000000000..0139bda9d0 --- /dev/null +++ b/store_test.go @@ -0,0 +1,197 @@ +package main + +import ( + "os" + "path" + "testing" + + none "github.com/docker/machine/drivers/none" + homedir "github.com/mitchellh/go-homedir" +) + +func clearHosts() error { + homeDir, err := homedir.Dir() + if err != nil { + return err + } + return os.RemoveAll(path.Join(homeDir, ".docker/hosts")) +} + +func TestStoreCreate(t *testing.T) { + if err := clearHosts(); err != nil { + t.Fatal(err) + } + + store := NewStore() + url := "unix:///var/run/docker.sock" + host, err := store.Create("test", "none", &none.CreateFlags{URL: &url}) + if err != nil { + t.Fatal(err) + } + if host.Name != "test" { + t.Fatal("Host name is incorrect") + } + homeDir, err := homedir.Dir() + if err != nil { + t.Fatal(err) + } + path := path.Join(homeDir, ".docker/hosts/test") + if _, err := os.Stat(path); os.IsNotExist(err) { + t.Fatalf("Host path doesn't exist: %s", path) + } +} + +func TestStoreRemove(t *testing.T) { + if err := clearHosts(); err != nil { + t.Fatal(err) + } + + store := NewStore() + url := "unix:///var/run/docker.sock" + _, err := store.Create("test", "none", &none.CreateFlags{URL: &url}) + if err != nil { + t.Fatal(err) + } + homeDir, err := homedir.Dir() + if err != nil { + t.Fatal(err) + } + path := path.Join(homeDir, ".docker/hosts/test") + if _, err := os.Stat(path); os.IsNotExist(err) { + t.Fatalf("Host path doesn't exist: %s", path) + } + err = store.Remove("test", false) + if err != nil { + t.Fatal(err) + } + if _, err := os.Stat(path); err == nil { + t.Fatalf("Host path still exists after remove: %s", path) + } +} + +func TestStoreList(t *testing.T) { + if err := clearHosts(); err != nil { + t.Fatal(err) + } + + store := NewStore() + url := "unix:///var/run/docker.sock" + _, err := store.Create("test", "none", &none.CreateFlags{URL: &url}) + if err != nil { + t.Fatal(err) + } + hosts, err := store.List() + if len(hosts) != 1 { + t.Fatalf("List returned %d items", len(hosts)) + } + if hosts[0].Name != "test" { + t.Fatalf("hosts[0] name is incorrect, got: %s", hosts[0].Name) + } +} + +func TestStoreExists(t *testing.T) { + if err := clearHosts(); err != nil { + t.Fatal(err) + } + + store := NewStore() + exists, err := store.Exists("test") + if exists { + t.Fatal("Exists returned true when it should have been false") + } + url := "unix:///var/run/docker.sock" + _, err = store.Create("test", "none", &none.CreateFlags{URL: &url}) + if err != nil { + t.Fatal(err) + } + exists, err = store.Exists("test") + if err != nil { + t.Fatal(err) + } + if !exists { + t.Fatal("Exists returned false when it should have been true") + } +} + +func TestStoreLoad(t *testing.T) { + if err := clearHosts(); err != nil { + t.Fatal(err) + } + + store := NewStore() + expectedURL := "unix:///foo/baz" + _, err := store.Create("test", "none", &none.CreateFlags{URL: &expectedURL}) + if err != nil { + t.Fatal(err) + } + + store = NewStore() + host, err := store.Load("test") + if host.Name != "test" { + t.Fatal("Host name is incorrect") + } + actualURL, err := host.GetURL() + if err != nil { + t.Fatal(err) + } + if actualURL != expectedURL { + t.Fatalf("GetURL is not %q, got %q", expectedURL, expectedURL) + } +} + +func TestStoreGetSetActive(t *testing.T) { + if err := clearHosts(); err != nil { + t.Fatal(err) + } + + store := NewStore() + + // No hosts set + host, err := store.GetActive() + if err != nil { + t.Fatal(err) + } + if host != nil { + t.Fatalf("GetActive: Active host should not exist") + } + + // Set normal host + url := "unix:///var/run/docker.sock" + originalHost, err := store.Create("test", "none", &none.CreateFlags{URL: &url}) + if err != nil { + t.Fatal(err) + } + + if err := store.SetActive(originalHost); err != nil { + t.Fatal(err) + } + + host, err = store.GetActive() + if err != nil { + t.Fatal(err) + } + if host.Name != "test" { + t.Fatalf("Active host is not 'test', got %s", host.Name) + } + isActive, err := store.IsActive(host) + if err != nil { + t.Fatal(err) + } + if isActive != true { + t.Fatal("IsActive: Active host is not test") + } + + // remove active host altogether + if err := store.RemoveActive(); err != nil { + t.Fatal(err) + } + + host, err = store.GetActive() + if err != nil { + t.Fatal(err) + } + if host != nil { + t.Fatalf("Active host is not nil", host.Name) + } + +}