diff --git a/.gitignore b/.gitignore index 2d18d02..45d168d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ nbproject *.sublime-project *.sublime-workspace *.swp +boot2docker-cli \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f6e163f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: go + +go: + - 1.4.1 + +# let us have pretty experimental Docker-based Travis workers +sudo: false + +env: + - GIMME_OS=linux GIMME_ARCH=amd64 + - GIMME_OS=darwin GIMME_ARCH=amd64 + - GIMME_OS=windows GIMME_ARCH=amd64 + +install: + - env | sort + - go get -d -v ./... + +script: + # TODO - some gofmt magic + - go build -v ./... + # TODO - go test -v ./... # (we first need to skip all the meaty vbox tests when "VBoxManage" isn't available, or mock it) diff --git a/DEPRECATION_WARNING b/DEPRECATION_WARNING new file mode 100644 index 0000000..32d7c87 --- /dev/null +++ b/DEPRECATION_WARNING @@ -0,0 +1,7 @@ + + WARNING: The 'boot2docker' command line interface (not to be confused with + 'boot2docker' the operating system) is officially deprecated. + + Please switch to Docker Machine (https://docs.docker.com/machine/) ASAP. + + Docker Toolbox (https://docker.com/toolbox) is the recommended install method. diff --git a/Dockerfile b/Dockerfile index b13e784..a5a19eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,13 @@ # Dockerfile to cross compile boot2docker-cli -FROM debian:jessie +FROM golang:1.4-cross -# Packaged dependencies -RUN apt-get update && apt-get install -y build-essential curl git - -# Install Go from binary release -RUN curl -sSL http://golang.org/dl/go1.3.src.tar.gz | tar -xzC /usr/local -ENV PATH /usr/local/go/bin:$PATH - -# Bootstrap Go for cross compilation (we have linux/amd64 by default) -ENV DOCKER_CROSSPLATFORMS darwin/amd64 windows/amd64 -RUN cd /usr/local/go/src && bash -xec 'for platform in $DOCKER_CROSSPLATFORMS; do GOOS=${platform%/*} GOARCH=${platform##*/} ./make.bash --no-clean 2>&1; done' - -ENV GOPATH /go -ADD . /go/src/github.com/boot2docker/boot2docker-cli WORKDIR /go/src/github.com/boot2docker/boot2docker-cli # Download (but not install) dependencies -RUN go get -d -v +RUN go get -v github.com/BurntSushi/toml +RUN go get -v github.com/ogier/pflag + +ADD . /go/src/github.com/boot2docker/boot2docker-cli CMD ["make", "all"] diff --git a/LICENSE b/LICENSE index e06d208..2744858 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ -Apache License + + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -175,18 +176,7 @@ Apache License END OF TERMS AND CONDITIONS - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} + 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. @@ -199,4 +189,3 @@ Apache License 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/Makefile b/Makefile index 1d69821..6d66304 100644 --- a/Makefile +++ b/Makefile @@ -12,14 +12,13 @@ default: dockerbuild @true # stop from matching "%" later -# Build binaries in Docker container. The `|| true` hack is a temporary fix for -# https://github.com/dotcloud/docker/issues/3986 +# Build binaries in Docker container. dockerbuild: clean docker build -t "$(DOCKER_IMAGE)" . docker run --name "$(DOCKER_CONTAINER)" "$(DOCKER_IMAGE)" - docker cp "$(DOCKER_CONTAINER)":"$(DOCKER_SRC_PATH)"/$(PREFIX)-$(VERSION)-darwin-$(GOARCH) . || true - docker cp "$(DOCKER_CONTAINER)":"$(DOCKER_SRC_PATH)"/$(PREFIX)-$(VERSION)-linux-$(GOARCH) . || true - docker cp "$(DOCKER_CONTAINER)":"$(DOCKER_SRC_PATH)"/$(PREFIX)-$(VERSION)-windows-$(GOARCH).exe . || true + docker cp "$(DOCKER_CONTAINER)":"$(DOCKER_SRC_PATH)"/$(PREFIX)-$(VERSION)-darwin-$(GOARCH) . + docker cp "$(DOCKER_CONTAINER)":"$(DOCKER_SRC_PATH)"/$(PREFIX)-$(VERSION)-linux-$(GOARCH) . + docker cp "$(DOCKER_CONTAINER)":"$(DOCKER_SRC_PATH)"/$(PREFIX)-$(VERSION)-windows-$(GOARCH).exe . docker rm "$(DOCKER_CONTAINER)" diff --git a/README.md b/README.md index d91760a..f9b5c5d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ +# Deprecated + +This project is officially deprecated in favor of [Docker +Machine](https://docs.docker.com/machine/). The code and documentation here +only exist as a reference for users who have not yet switched over (but please +do soon). The recommended way to install Machine is with the [Docker +Toolbox](https://docker.com/toolbox). + # boot2docker command line management tool This tool downloads the boot2docker ISO image, creates a VirtualBox virtual @@ -9,7 +17,7 @@ login via SSH. On Windows, [MSYS SSH](http://www.mingw.org/) provides a first class way to connect to the boot2docker VM using `boot2docker.exe ssh`. -> **Note:** Docker now has an [IANA registered IP Port: 2375]( http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=docker) +> **Note:** Docker now has an [IANA registered IP Port: 2376]( http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=docker) > , so the use of port 4243 is deprecated. This also means that new Boot2Docker > ISO releases and management tool are not compatible. @@ -19,17 +27,34 @@ connect to the boot2docker VM using `boot2docker.exe ssh`. Signed installers are available for [Mac OS X](http://github.com/boot2docker/osx-installer/releases) and [Windows](http://github.com/boot2docker/windows-installer/releases). -Refer to the installation instructions for [Windows](http://docs.docker.io/installation/windows/) and [Mac OS X](http://docks.docker.io/mac/). +Refer to the installation instructions for [Mac OS X](http://docs.docker.io/installation/mac/) and [Windows](http://docs.docker.io/installation/windows/). ### Manual Installation ### Pre-compiled binaries -You can dowload binary releases at https://github.com/boot2docker/boot2docker-cli/releases +You can download binary releases at https://github.com/boot2docker/boot2docker-cli/releases + +### Docker container build + +The most reliable way to cross-compile boot2docker for all 3 platforms, is to +use the Dockerfile (and Docker) + +```sh +git clone https://github.com/boot2docker/boot2docker-cli +cd boot2docker-cli +make +``` + +Built binaries will be available in the current directory. + +This assumes you have an accessible Docker daemon (local or remote with `DOCKER_HOST` set) +and `make` installed. + ### Install from source -You need to have [Go compiler](http://golang.org) installed, and `$GOPATH` +You need to have the [Go compiler (v1.4 or higher)](http://golang.org) installed, and `$GOPATH` [properly setup](http://golang.org/doc/code.html#GOPATH). Then run go get github.com/boot2docker/boot2docker-cli @@ -52,6 +77,9 @@ You can cross compile to OS X, Windows, and Linux. For that you need to first [make your Go compiler ready for cross compiling to the target platforms](http://stackoverflow.com/questions/12168873/cross-compile-go-on-osx). +Please make sure you build with golang v1.4 or later - it is required for +`boot2docker download` to work on OS X. + We provide a Makefile to make the process a bit easier. ```sh @@ -65,28 +93,6 @@ make clean # clean up the built binaries Built binaries will be available in the current directory. -### Docker build - -You can also build in a Docker container. - -```sh -make dockerbuild -``` - -Built binaries will be available in the current directory. - - -### Caveats - -Currently the binary cross-compiled from Windows/Linux to OS X has a [TLS -issue](https://github.com/boot2docker/boot2docker-cli/issues/11), and as a -result - - $ boot2docker download - -will fail. You need to do a native OS X build to avoid this problem. - - ## Usage To initialize a new boot2docker VM, run @@ -122,10 +128,10 @@ boot2docker vm, which you can then use to access ports you map from your contain The `boot2docker` binary reads configuration from `$BOOT2DOCKER_PROFILE` if set, or `$BOOT2DOCKER_DIR/profile` or `$HOME/.boot2docker/profile` or (on Windows) -`$USERPROFILE/.boot2docker/profile`. `boot2docker config` will +`%USERPROFILE%/.boot2docker/profile`. `boot2docker config` will tell you where it is looking for the file, and will also output the settings that -are in use, so you can initialise a default file to customise using -`boot2docker config > /home/sven/.boot2docker/profile`. +are in use, so you can initialise a default file to customize using +`boot2docker config > ~/.boot2docker/profile`. Currently you can configure the following options (undefined options take default values): @@ -144,6 +150,13 @@ SSHKey = "/Users/sven/.ssh/id_boot2docker" # name of boot2docker virtual machine VM = "boot2docker-vm" +# URL pointing either to a Github "/releases" API endpoint to automatically +# retrieve the `boot2docker.iso` asset from the latest released version of a +# repo, or directly to an ISO image +ISOURL = "https://api.github.com/repos/boot2docker/boot2docker/releases" +#ISOURL = "https://github.com/boot2docker/boot2docker/releases/download/v1.0.0/boot2docker.iso" +#ISOURL = "https://internal.corp.org/b2d.iso" + # path to boot2docker ISO image ISO = "/Users/sven/.boot2docker/boot2docker.iso" @@ -153,11 +166,14 @@ DiskSize = 20000 # VM memory size in MB Memory = 2048 +# Number of CPUs +CPUs = 1 + # host port forwarding to port 22 in the VM SSHPort = 2022 -# host port forwarding to port 2375 in the VM -DockerPort = 2375 +# host port forwarding to port 2376 in the VM +DockerPort = 2376 # host-only network host IP HostIP = "192.168.59.3" @@ -182,10 +198,46 @@ You can override the configurations using matching command-line flags. Type `boot2docker -h` for more information. The configuration file options are the same as the command-line flags with long names. +## Upgrade + +You can use boot2docker-cli to upgrade: + +1. The ISO you are using in the VM (and consequently the Docker daemon version) +2. The Docker client binary on your host system +3. The boot2docker-cli binary itself + +To do so, run the `boot2docker upgrade` command. + +```console +$ boot2docker upgrade +Backing up existing docker binary... +Downloading new docker client binary... +Success: downloaded https://get.docker.com/builds/Darwin/x86_64/docker-latest + to /usr/local/bin/docker + The old version is backed up to ~/.boot2docker. +Backing up existing boot2docker binary... +Downloading new boot2docker client binary... +Success: downloaded https://github.com/boot2docker/boot2docker-cli/releases/download/v1.4.0/boot2docker-v1.4.0-darwin-amd64 + to /usr/local/bin/boot2docker + The old version is backed up to ~/.boot2docker. +Latest release for boot2docker/boot2docker is v1.4.0 +Downloading boot2docker ISO image... +Success: downloaded https://github.com/boot2docker/boot2docker/releases/download/v1.4.0/boot2docker.iso + to /Users/youruser/.boot2docker/boot2docker.iso +Waiting for VM and Docker daemon to start... +.................ooo +Started. +``` + +This will back up your current `docker` and `boot2docker` binaries to +`~/.boot2docker` and download the latest ISO, `docker` binary and `boot2docker` +binary in place of the old versions. ## Contribution +[![Build Status](https://travis-ci.org/boot2docker/boot2docker-cli.svg?branch=master)](https://travis-ci.org/boot2docker/boot2docker-cli) + We are implementing the same process as [Docker merge approval](https://github.com/dotcloud/docker/blob/master/CONTRIBUTING.md#merge-approval), so all commits need to be done via pull requests, and will need three or more diff --git a/VERSION b/VERSION index b18d465..804a616 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.0.1 +v1.8.0 diff --git a/cmds.go b/cmds.go index ad68f4c..3cd391e 100644 --- a/cmds.go +++ b/cmds.go @@ -1,432 +1,540 @@ package main import ( - "archive/tar" - "bytes" "encoding/json" "fmt" "io/ioutil" - "net" "os" + "os/exec" "path/filepath" + "regexp" "runtime" "strings" "time" - vbx "github.com/boot2docker/boot2docker-cli/virtualbox" + "github.com/boot2docker/boot2docker-cli/driver" + _ "github.com/boot2docker/boot2docker-cli/dummy" + _ "github.com/boot2docker/boot2docker-cli/virtualbox" ) +func vmNotRunningError(vmName string) error { + return fmt.Errorf("VM %q is not running. (Did you run `boot2docker up`?)", vmName) +} + // Initialize the boot2docker VM from scratch. -func cmdInit() int { - // TODO(@riobard) break up this command into multiple stages - m, err := vbx.GetMachine(B2D.VM) +func cmdInit() error { + B2D.Init = false + _, err := driver.GetMachine(&B2D) if err == nil { - logf("Virtual machine %s already exists", B2D.VM) - return 1 - } - - if ping(fmt.Sprintf("localhost:%d", B2D.DockerPort)) { - logf("--dockerport=%d on localhost is occupied. Please choose another one.", B2D.DockerPort) - return 1 - } - - if ping(fmt.Sprintf("localhost:%d", B2D.SSHPort)) { - logf("--sshport=%d on localhost is occupied. Please choose another one.", B2D.SSHPort) - return 1 + fmt.Printf("Virtual machine %s already exists\n", B2D.VM) + return nil } if _, err := os.Stat(B2D.ISO); err != nil { if !os.IsNotExist(err) { - logf("Failed to open ISO image %q: %s", B2D.ISO, err) - return 1 + return fmt.Errorf("Failed to open ISO image %q: %s", B2D.ISO, err) } - if exitcode := cmdDownload(); exitcode != 0 { - return exitcode + if err := cmdDownload(); err != nil { + return err } } + if _, err := os.Stat(B2D.SSHKey); err != nil { if !os.IsNotExist(err) { - logf("Something wrong with SSH Key file %q: %s", B2D.SSHKey, err) - return 1 + return fmt.Errorf("Something wrong with SSH Key file %q: %s", B2D.SSHKey, err) } - if err := cmdInteractive(B2D.SSHGen, "-t", "rsa", "-N", "", "-f", B2D.SSHKey); err != nil { - logf("Error generating new SSH Key into %s: %s", B2D.SSHKey, err) - return 1 + + cmd := exec.Command(B2D.SSHGen, "-t", "rsa", "-N", "", "-f", B2D.SSHKey) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if B2D.Verbose { + cmd.Stderr = os.Stderr + fmt.Printf("executing: %v %v\n", cmd.Path, strings.Join(cmd.Args, " ")) + } + + if err := cmd.Run(); err != nil { + return fmt.Errorf("Error generating new SSH Key into %s: %s", B2D.SSHKey, err) } } //TODO: print a ~/.ssh/config entry for our b2d connection that the user can c&p - logf("Creating VM %s...", B2D.VM) - m, err = vbx.CreateMachine(B2D.VM, "") + B2D.Init = true + _, err = driver.GetMachine(&B2D) if err != nil { - logf("FIRST: Failed to create VM %q: %s", B2D.VM, err) - // double tap. VBox will sometimes (like on initial install, or reboot) - // fail to start the service the first time. - m, err = vbx.CreateMachine(B2D.VM, "") - if err != nil { - logf("Failed to create VM %q: %s", B2D.VM, err) - return 1 - } + return fmt.Errorf("Failed to initialize machine %q: %s", B2D.VM, err) } + fmt.Printf("Initialization of virtual machine %q complete.\n", B2D.VM) + fmt.Printf("Use `boot2docker up` to start it.\n") + return nil +} - logf("Apply interim patch to VM %s (https://www.virtualbox.org/ticket/12748)", B2D.VM) - if err := vbx.SetExtra(B2D.VM, "VBoxInternal/CPUM/EnableHVP", "1"); err != nil { - logf("Failed to patch vm: %s", err) - return 1 +// Bring up the VM from all possible states. +func cmdUp() error { + m, err := driver.GetMachine(&B2D) + if err != nil { + return fmt.Errorf("Failed to get machine %q: %s", B2D.VM, err) + } + if err := m.Start(); err != nil { + return fmt.Errorf("Failed to start machine %q: %s", B2D.VM, err) } - m.OSType = "Linux26_64" - m.CPUs = uint(runtime.NumCPU()) - m.Memory = B2D.Memory - m.SerialFile = B2D.SerialFile - - m.Flag |= vbx.F_pae - m.Flag |= vbx.F_longmode // important: use x86-64 processor - m.Flag |= vbx.F_rtcuseutc - m.Flag |= vbx.F_acpi - m.Flag |= vbx.F_ioapic - m.Flag |= vbx.F_hpet - m.Flag |= vbx.F_hwvirtex - m.Flag |= vbx.F_vtxvpid - m.Flag |= vbx.F_largepages - m.Flag |= vbx.F_nestedpaging + if err := m.Refresh(); err != nil { + return fmt.Errorf("Failed to start machine %q: %s", B2D.VM, err) + } + if m.GetState() != driver.Running { + return fmt.Errorf("Failed to start machine %q (run again with -v for details)", B2D.VM) + } - m.BootOrder = []string{"dvd"} - if err := m.Modify(); err != nil { - logf("Failed to modify VM %q: %s", B2D.VM, err) - return 1 + fmt.Println("Waiting for VM and Docker daemon to start...") + //give the VM a little time to start, so we don't kill the Serial Pipe/Socket + time.Sleep(time.Duration(B2D.Waittime) * time.Millisecond) + natSSH := fmt.Sprintf("localhost:%d", m.GetSSHPort()) + IP := "" + for i := 1; i < B2D.Retries; i++ { + print(".") + if B2D.Serial && runtime.GOOS != "windows" { + if IP, err = RequestIPFromSerialPort(m.GetSerialFile()); err == nil { + break + } + } + if err := read(natSSH, 1, time.Duration(B2D.Waittime)*time.Millisecond); err == nil { + if IP, err = RequestIPFromSSH(m); err == nil { + break + } + } + } + if B2D.Verbose { + fmt.Printf("VM Host-only IP address: %s", IP) + fmt.Printf("\nWaiting for Docker daemon to start...\n") } - logf("Setting NIC #1 to use NAT network...") - if err := m.SetNIC(1, vbx.NIC{Network: vbx.NICNetNAT, Hardware: vbx.VirtIO}); err != nil { - logf("Failed to add network interface to VM %q: %s", B2D.VM, err) - return 1 + time.Sleep(time.Duration(B2D.Waittime) * time.Millisecond) + socket := "" + for i := 1; i < B2D.Retries; i++ { + print("o") + if socket, err = RequestSocketFromSSH(m); err == nil { + break + } + if B2D.Verbose { + fmt.Printf("Error requesting socket: %s\n", err) + } + time.Sleep(600 * time.Millisecond) } + fmt.Printf("\nStarted.\n") + + if socket == "" { + // lets try one more time + time.Sleep(time.Duration(B2D.Waittime) * time.Millisecond) + fmt.Printf(" Trying to get Docker socket one more time\n") - pfRules := map[string]vbx.PFRule{ - "ssh": {Proto: vbx.PFTCP, HostIP: net.ParseIP("127.0.0.1"), HostPort: B2D.SSHPort, GuestPort: 22}, - "docker": {Proto: vbx.PFTCP, HostIP: net.ParseIP("127.0.0.1"), HostPort: B2D.DockerPort, GuestPort: 2375}, + if socket, err = RequestSocketFromSSH(m); err != nil { + fmt.Printf("Error requesting socket: %s\n", err) + } + } + // Copying the certs here - someone might have have written a Windows API client. + certPath, err := RequestCertsUsingSSH(m) + if err != nil { + // These errors are not fatal + fmt.Fprintf(os.Stderr, "Warning: error copying certificates: %s\n", err) } - for name, rule := range pfRules { - if err := m.AddNATPF(1, name, rule); err != nil { - logf("Failed to add port forwarding to VM %q: %s", B2D.VM, err) - return 1 + if socket == "" { + fmt.Fprintf(os.Stderr, "Auto detection of the VM's Docker socket failed.\n") + fmt.Fprintf(os.Stderr, "Please run `boot2docker -v up` to diagnose.\n") + } else { + // Check if $DOCKER_* ENV vars are properly configured. + if !checkEnvironment(socket, certPath) { + fmt.Printf("\nTo connect the Docker client to the Docker daemon, please set:\n") + printExport(socket, certPath) + fmt.Printf("\nOr run: `eval \"$(boot2docker shellinit)\"`\n") + } else { + fmt.Printf("Your environment variables are already set correctly.\n") } - logf("Port forwarding [%s] %s", name, rule) } + fmt.Printf("\n") + return nil +} - hostIFName, err := getHostOnlyNetworkInterface() +// Give the user the exact command to run to set the env. +func cmdShellInit() error { + m, err := driver.GetMachine(&B2D) if err != nil { - logf("Failed to create host-only network interface: %s", err) - return 1 + return fmt.Errorf("Failed to get machine %q: %s", B2D.VM, err) } - logf("Setting NIC #2 to use host-only network %q...", hostIFName) - if err := m.SetNIC(2, vbx.NIC{Network: vbx.NICNetHostonly, Hardware: vbx.VirtIO, HostonlyAdapter: hostIFName}); err != nil { - logf("Failed to add network interface to VM %q: %s", B2D.VM, err) - return 1 + if m.GetState() != driver.Running { + return vmNotRunningError(B2D.VM) } - logf("Setting VM storage...") - if err := m.AddStorageCtl("SATA", vbx.StorageController{SysBus: vbx.SysBusSATA, HostIOCache: true, Bootable: true}); err != nil { - logf("Failed to add storage controller to VM %q: %s", B2D.VM, err) - return 1 + socket, err := RequestSocketFromSSH(m) + if err != nil { + return fmt.Errorf("Error requesting socket: %s\n", err) } - if err := m.AttachStorage("SATA", vbx.StorageMedium{Port: 0, Device: 0, DriveType: vbx.DriveDVD, Medium: B2D.ISO}); err != nil { - logf("Failed to attach ISO image %q: %s", B2D.ISO, err) - return 1 + certPath, err := RequestCertsUsingSSH(m) + if err != nil { + // These errors are not fatal + fmt.Fprintf(os.Stderr, "Warning: error copying certificates: %s\n", err) } - diskImg := filepath.Join(m.BaseFolder, fmt.Sprintf("%s.vmdk", B2D.VM)) + // Check if $DOCKER_* ENV vars are properly configured. + if !checkEnvironment(socket, certPath) { + printExport(socket, certPath) + } else { + fmt.Fprintf(os.Stderr, "Your environment variables are already set correctly.\n") + } - if _, err := os.Stat(diskImg); err != nil { - if !os.IsNotExist(err) { - logf("Failed to open disk image %q: %s", diskImg, err) - return 1 + return nil +} + +func checkEnvironment(socket, certPath string) bool { + for name, value := range exports(socket, certPath) { + value = strings.Trim(value, `"'`) // remove surrounding quotes + if os.Getenv(name) != value { + return false } + } - if B2D.VMDK != "" { - logf("Using %v as base VMDK", B2D.VMDK) - if err := copyDiskImage(diskImg, B2D.VMDK); err != nil { - logf("Failed to copy disk image %v from %v: %s", diskImg, B2D.VMDK, err) - return 1 - } - } else { - magicString := "boot2docker, please format-me" + return true +} - buf := new(bytes.Buffer) - tw := tar.NewWriter(buf) +func printExport(socket, certPath string) { + values := exports(socket, certPath) + if runtime.GOOS == "windows" && !isUnixShellOnWindows() { + printExportWindows(values) + } else { + printExportUnix(values) + } +} - // 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 { - logf("Error making tarfile: %s", err) - return 1 - } - if _, err := tw.Write([]byte(magicString)); err != nil { - logf("Error making tarfile: %s", err) - return 1 - } - // .ssh/key.pub => authorized_keys - file = &tar.Header{Name: ".ssh", Typeflag: tar.TypeDir, Mode: 0700} - if err := tw.WriteHeader(file); err != nil { - logf("Error making tarfile: %s", err) - return 1 - } - pubKey, err := ioutil.ReadFile(B2D.SSHKey + ".pub") - if err != nil { - logf("Error making tarfile: %s", err) - return 1 - } - file = &tar.Header{Name: ".ssh/authorized_keys", Size: int64(len(pubKey)), Mode: 0644} - if err := tw.WriteHeader(file); err != nil { - logf("Error making tarfile: %s", err) - return 1 - } - if _, err := tw.Write([]byte(pubKey)); err != nil { - logf("Error making tarfile: %s", err) - return 1 - } - file = &tar.Header{Name: ".ssh/authorized_keys2", Size: int64(len(pubKey)), Mode: 0644} - if err := tw.WriteHeader(file); err != nil { - logf("Error making tarfile: %s", err) - return 1 +func isUnixShellOnWindows() bool { + return os.Getenv("TERM") != "" +} + +func printExportUnix(values map[string]string) { + for name, value := range values { + switch filepath.Base(os.Getenv("SHELL")) { + case "fish": + if value == "" { + fmt.Printf(" set -e %s\n", name) + } else { + fmt.Printf(" set -x %s %s\n", name, value) } - if _, err := tw.Write([]byte(pubKey)); err != nil { - logf("Error making tarfile: %s", err) - return 1 + default: // default command to export variables POSIX shells, like bash, zsh, etc. + if value == "" { + fmt.Printf(" unset %s\n", name) + } else { + fmt.Printf(" export %s=%s\n", name, value) } - if err := tw.Close(); err != nil { - logf("Error making tarfile: %s", err) - return 1 + } + } +} + +func printExportWindows(values map[string]string) { + // Print cmd.exe instructions to stderr + fmt.Fprintln(os.Stderr, "If you are running inside Windows Command Prompt (cmd.exe), copy and paste the") + fmt.Fprintln(os.Stderr, "following commands to your terminal to set the environment variables:") + for name, value := range values { + if value == "" { + fmt.Fprintf(os.Stderr, " set %s=\n", name) + } else { + fmt.Fprintf(os.Stderr, " set %s=%s\n", name, value) + } + } + fmt.Fprintln(os.Stderr, "") + // Print powershell instructions to stderr + fmt.Fprintln(os.Stderr, "If you are running inside PowerShell, copy or paste the following commands") + fmt.Fprintln(os.Stderr, `to your shell or run "boot2docker shellinit | Invoke-Expression" to set the`) + fmt.Fprintln(os.Stderr, "environment variables:") + + // Print powershell exports to stdout + for name, value := range values { + if value == "" { + fmt.Printf(" Remove-Item Env:\\%s\n", name) + } else { + fmt.Printf(" $Env:%s = \"%s\"\n", name, value) + } + } +} + +func exports(socket, certPath string) map[string]string { + out := make(map[string]string) + + out["DOCKER_HOST"] = socket + out["DOCKER_CERT_PATH"] = certPath + + if runtime.GOOS == "windows" && isUnixShellOnWindows() { + // Surround Windows-style paths with single quotes in exported path otherwise + // bash swallows the backslashes in export statements like: + // export DOCKER_CERT_PATH=C:\Users\ahmet\.boot2docker\certs\boot2docker-vm + out["DOCKER_CERT_PATH"] = fmt.Sprintf("'%s'", out["DOCKER_CERT_PATH"]) + } + + if certPath == "" { + out["DOCKER_TLS_VERIFY"] = "" + } else { + out["DOCKER_TLS_VERIFY"] = "1" + } + + //if a http_proxy is set, we need to make sure the boot2docker ip + //is added to the NO_PROXY environment variable + if os.Getenv("http_proxy") != "" || os.Getenv("HTTP_PROXY") != "" { + //get the ip from socket/DOCKER_HOST + re := regexp.MustCompile("tcp://([^:]+):") + if matches := re.FindStringSubmatch(socket); len(matches) == 2 { + ip := matches[1] + name := "no_proxy" + val := os.Getenv("no_proxy") + if val == "" { + name = "NO_PROXY" + val = os.Getenv("NO_PROXY") } - if err := makeDiskImage(diskImg, B2D.DiskSize, buf.Bytes()); err != nil { - logf("Failed to create disk image %q: %s", diskImg, err) - return 1 + switch { + case val == "": + out[name] = ip + case strings.Contains(val, ip): + out[name] = val + default: + out[name] = fmt.Sprintf("%s,%s", val, ip) } } } + return out +} - if err := m.AttachStorage("SATA", vbx.StorageMedium{Port: 1, Device: 0, DriveType: vbx.DriveHDD, Medium: diskImg}); err != nil { - logf("Failed to attach disk image %q: %s", diskImg, err) - return 1 +// Tell the user the config (and later let them set it?) +func cmdConfig() error { + dir, err := cfgDir(".boot2docker") + if err != nil { + return fmt.Errorf("Error working out Profile file location: %s\n", err) } + filename := cfgFilename(dir) + fmt.Printf("# boot2docker profile filename: %s\n", filename) + fmt.Println(printConfig()) + return nil +} - logf("Done. Type `%s up` to start the VM.", os.Args[0]) - return 0 +// Suspend and save the current state of VM on disk. +func cmdSave() error { + m, err := driver.GetMachine(&B2D) + if err != nil { + return fmt.Errorf("Failed to get machine %q: %s\n", B2D.VM, err) + } + if err := m.Save(); err != nil { + return fmt.Errorf("Failed to save machine %q: %s\n", B2D.VM, err) + } + return nil } -// Bring up the VM from all possible states. -func cmdUp() int { - m, err := vbx.GetMachine(B2D.VM) +// Gracefully stop the VM by sending ACPI shutdown signal. +func cmdStop() error { + m, err := driver.GetMachine(&B2D) if err != nil { - logf("Failed to get machine %q: %s", B2D.VM, err) - return 2 + return fmt.Errorf("Failed to get machine %q: %s", B2D.VM, err) } - if err := m.Start(); err != nil { - logf("Failed to start machine %q: %s", B2D.VM, err) - return 1 + if err := m.Stop(); err != nil { + return fmt.Errorf("Failed to stop machine %q: %s", B2D.VM, err) } + return nil +} - if err := m.Refresh(); err != nil { - logf("Failed to start machine %q: %s", B2D.VM, err) - return 1 +// Forcefully power off the VM (equivalent to unplug power). Might corrupt disk +// image. +func cmdPoweroff() error { + m, err := driver.GetMachine(&B2D) + if err != nil { + return fmt.Errorf("Failed to get machine %q: %s", B2D.VM, err) } - if m.State != vbx.Running { - logf("Failed to start machine %q (run again with -v for details)", B2D.VM) - return 1 + if err := m.Poweroff(); err != nil { + return fmt.Errorf("Failed to poweroff machine %q: %s", B2D.VM, err) } + return nil +} - logf("Waiting for VM to be started...") - //give the VM a little time to start, so we don't kill the Serial Pipe/Socket - time.Sleep(2) - natSSH := fmt.Sprintf("localhost:%d", B2D.SSHPort) - IP := "" - for i := 1; i < 30; i++ { - if B2D.Serial && runtime.GOOS != "windows" { - if IP = RequestIPFromSerialPort(m.SerialFile); IP != "" { - break +// Upgrade the boot2docker ISO - preserving server state +func cmdUpgrade() error { + if err := upgradeBoot2DockerBinary(); err != nil { + return fmt.Errorf("Error upgrading boot2docker binary: %s", err) + } + m, err := driver.GetMachine(&B2D) + if err == nil { + if m.GetState() == driver.Running || m.GetState() == driver.Saved || m.GetState() == driver.Paused { + // Windows won't let us move the ISO aside while it's in use + if err = cmdStop(); err == nil { + if err = cmdDownload(); err == nil { + err = cmdUp() + } } + return err } - if err := read(natSSH, 1, 2*time.Second); err == nil { - IP = RequestIPFromSSH(m) - break - } - - print(".") } - print("\n") + return cmdDownload() +} - logf("Started.") +func upgradeBoot2DockerBinary() error { + var ( + goos, arch, ext string + ) + latestVersion, err := getLatestReleaseName("https://api.github.com/repos/boot2docker/boot2docker-cli/releases") + if err != nil { + return fmt.Errorf("Error attempting to get the latest boot2docker-cli release: %s", err) + } + baseUrl := "https://github.com/boot2docker/boot2docker-cli/releases/download" - if IP == "" { - // lets try one more time - time.Sleep(3) + ext = "" - IP = RequestIPFromSSH(m) - if IP == "" { - logf("Auto detection of the VM's IP address failed.") - } + switch runtime.GOARCH { + case "amd64": + arch = "amd64" + default: + return fmt.Errorf("Architecture not supported") } + switch runtime.GOOS { + case "darwin", "linux": + goos = runtime.GOOS case "windows": - logf("Docker client does not run on Windows for now. Please use") - logf(" %s ssh", os.Args[0]) - logf("to SSH into the VM instead.") + goos = "windows" + arch = "amd64" + ext = ".exe" default: - // Check if $DOCKER_HOST ENV var is properly configured. - if os.Getenv("DOCKER_HOST") != fmt.Sprintf("tcp://%s:%d", IP, m.DockerPort) { - logf("To connect the Docker client to the Docker daemon, please set:") - logf(" export DOCKER_HOST=tcp://%s:%d", IP, m.DockerPort) - } else { - logf("Your DOCKER_HOST env variable is already set correctly.") - } + return fmt.Errorf("Operating system not supported") } - return 0 + binaryUrl := fmt.Sprintf("%s/%s/boot2docker-%s-%s-%s%s", baseUrl, latestVersion, latestVersion, goos, arch, ext) + currentBoot2DockerVersion := Version + if err := attemptUpgrade(binaryUrl, "boot2docker", latestVersion, currentBoot2DockerVersion); err != nil { + return fmt.Errorf("Error attempting upgrade: %s", err) + } + return nil } -// Tell the user the config (and later let them set it?) -func cmdConfig() int { - dir, err := getCfgDir(".boot2docker") - if err != nil { - logf("Error working out Profile file location: %s", err) - return 1 +func attemptUpgrade(binaryUrl, binaryName, latestVersion, localVersion string) error { + if (latestVersion != localVersion && !strings.Contains(latestVersion, "rc")) || B2D.ForceUpgradeDownload { + if err := backupAndDownload(binaryUrl, binaryName, localVersion); err != nil { + return fmt.Errorf("Error attempting backup and download of Docker client binary: %s", err) + } + } else { + fmt.Printf("%s is up to date (%s), skipping upgrade...\n", binaryName, localVersion) } - filename := getCfgFilename(dir) - logf("boot2docker profile filename: %s", filename) - fmt.Println(printConfig()) - return 0 + return nil } -// Suspend and save the current state of VM on disk. -func cmdSave() int { - m, err := vbx.GetMachine(B2D.VM) +func backupAndDownload(binaryUrl, binaryName, localVersion string) error { + binaryPath, err := exec.LookPath(binaryName) if err != nil { - logf("Failed to get machine %q: %s", B2D.VM, err) - return 2 + return fmt.Errorf("Error attempting to locate local binary: %s", err) } - if err := m.Save(); err != nil { - logf("Failed to save machine %q: %s", B2D.VM, err) - return 1 + path := strings.TrimSpace(string(binaryPath)) + + fmt.Println("Backing up existing", binaryName, "binary...") + if err := backupBinary(binaryName, localVersion, path); err != nil { + return fmt.Errorf("Error backing up docker client: %s", err) } - return 0 -} -// Gracefully stop the VM by sending ACPI shutdown signal. -func cmdStop() int { - m, err := vbx.GetMachine(B2D.VM) - if err != nil { - logf("Failed to get machine %q: %s", B2D.VM, err) - return 2 + fmt.Println("Downloading new", binaryName, "client binary...") + if err := download(path, binaryUrl); err != nil { + return fmt.Errorf("Error attempting to download new client binary: %s", err) } - if err := m.Stop(); err != nil { - logf("Failed to stop machine %q: %s", B2D.VM, err) - return 1 + if err := os.Chmod(path, 0755); err != nil { + return err } - return 0 + fmt.Printf("Success: downloaded %s\n\tto %s\n\tThe old version is backed up to ~/.boot2docker.\n", binaryUrl, path) + return nil } -// Forcefully power off the VM (equivalent to unplug power). Might corrupt disk -// image. -func cmdPoweroff() int { - m, err := vbx.GetMachine(B2D.VM) +func backupBinary(binaryName, localVersion, path string) error { + dir, err := cfgDir(".boot2docker") if err != nil { - logf("Failed to get machine %q: %s", B2D.VM, err) - return 2 + return fmt.Errorf("Error getting boot2docker config dir: %s", err) } - if err := m.Poweroff(); err != nil { - logf("Failed to poweroff machine %q: %s", B2D.VM, err) - return 1 + buf, err := ioutil.ReadFile(path) + if err != nil { + return fmt.Errorf("Error opening binary for reading at %s: %s", path, err) + } + backupName := fmt.Sprintf("%s-%s", binaryName, localVersion) + if err := ioutil.WriteFile(filepath.Join(dir, backupName), buf, 0755); err != nil { + return fmt.Errorf("Error creating backup file: %s", err) } - return 0 + return nil } // Gracefully stop and then start the VM. -func cmdRestart() int { - m, err := vbx.GetMachine(B2D.VM) +func cmdRestart() error { + m, err := driver.GetMachine(&B2D) if err != nil { - logf("Failed to get machine %q: %s", B2D.VM, err) - return 2 + return fmt.Errorf("Failed to get machine %q: %s", B2D.VM, err) } if err := m.Restart(); err != nil { - logf("Failed to restart machine %q: %s", B2D.VM, err) - return 1 + return fmt.Errorf("Failed to restart machine %q: %s", B2D.VM, err) } - return 0 + return nil } // Forcefully reset (equivalent to cold boot) the VM. Might corrupt disk image. -func cmdReset() int { - m, err := vbx.GetMachine(B2D.VM) +func cmdReset() error { + m, err := driver.GetMachine(&B2D) if err != nil { - logf("Failed to get machine %q: %s", B2D.VM, err) - return 2 + return fmt.Errorf("Failed to get machine %q: %s", B2D.VM, err) } if err := m.Reset(); err != nil { - logf("Failed to reset machine %q: %s", B2D.VM, err) - return 1 + return fmt.Errorf("Failed to reset machine %q: %s", B2D.VM, err) } - return 0 + return nil } // Delete the VM and associated disk image. -func cmdDelete() int { - m, err := vbx.GetMachine(B2D.VM) +func cmdDelete() error { + m, err := driver.GetMachine(&B2D) if err != nil { - if err == vbx.ErrMachineNotExist { - logf("Machine %q does not exist.", B2D.VM) - return 0 + if err == driver.ErrMachineNotExist { + return fmt.Errorf("Machine %q does not exist.", B2D.VM) } - logf("Failed to get machine %q: %s", B2D.VM, err) - return 2 + return fmt.Errorf("Failed to get machine %q: %s", B2D.VM, err) } if err := m.Delete(); err != nil { - logf("Failed to delete machine %q: %s", B2D.VM, err) - return 1 + return fmt.Errorf("Failed to delete machine %q: %s", B2D.VM, err) } - return 0 + return nil } // Show detailed info of the VM. -func cmdInfo() int { - m, err := vbx.GetMachine(B2D.VM) +func cmdInfo() error { + m, err := driver.GetMachine(&B2D) if err != nil { - logf("Failed to get machine %q: %s", B2D.VM, err) - return 2 + return fmt.Errorf("Failed to get machine %q: %s", B2D.VM, err) } - if err := json.NewEncoder(os.Stdout).Encode(m); err != nil { - logf("Failed to encode machine %q info: %s", B2D.VM, err) - return 1 + b, err := json.MarshalIndent(m, "", "\t") + if err != nil { + return fmt.Errorf("Failed to encode machine %q info: %s", B2D.VM, err) } - return 0 + + os.Stdout.Write(b) + + return nil } // Show the current state of the VM. -func cmdStatus() int { - m, err := vbx.GetMachine(B2D.VM) +func cmdStatus() error { + m, err := driver.GetMachine(&B2D) if err != nil { - logf("Failed to get machine %q: %s", B2D.VM, err) - return 2 + return fmt.Errorf("Failed to get machine %q: %s", B2D.VM, err) } - fmt.Println(m.State) - return 0 + fmt.Println(m.GetState()) + return nil } // Call the external SSH command to login into boot2docker VM. -func cmdSSH() int { - m, err := vbx.GetMachine(B2D.VM) +func cmdSSH() error { + m, err := driver.GetMachine(&B2D) if err != nil { - logf("Failed to get machine %q: %s", B2D.VM, err) - return 2 + return fmt.Errorf("Failed to get machine %q: %s", B2D.VM, err) } - if m.State != vbx.Running { - logf("VM %q is not running.", B2D.VM) - return 1 + if m.GetState() != driver.Running { + return vmNotRunningError(B2D.VM) } // find the ssh cmd string and then pass any remaining strings to ssh @@ -437,103 +545,76 @@ func cmdSSH() int { i++ } - sshArgs := append([]string{ - "-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", m.SSHPort), - "-i", B2D.SSHKey, - "docker@localhost", - }, os.Args[i:]...) - - if err := cmdInteractive(B2D.SSH, sshArgs...); err != nil { - logf("%s", err) - return 1 + if err := cmdInteractive(m, os.Args[i:]...); err != nil { + return fmt.Errorf("%s", err) } - return 0 + return nil } -func cmdIP() int { - m, err := vbx.GetMachine(B2D.VM) +func cmdIP() error { + m, err := driver.GetMachine(&B2D) if err != nil { - logf("Failed to get machine %q: %s", B2D.VM, err) - return 2 + return fmt.Errorf("Failed to get machine %q: %s", B2D.VM, err) } - if m.State != vbx.Running { - logf("VM %q is not running.", B2D.VM) - return 1 + if m.GetState() != driver.Running { + return vmNotRunningError(B2D.VM) } IP := "" if B2D.Serial { - for i := 1; i < 20; i++ { - if runtime.GOOS != "windows" { - if IP = RequestIPFromSerialPort(m.SerialFile); IP != "" { - break + if runtime.GOOS != "windows" { + if IP, err = RequestIPFromSerialPort(m.GetSerialFile()); err != nil { + if B2D.Verbose { + fmt.Printf("Error getting IP via Serial: %s\n", err) } } } } if IP == "" { - IP = RequestIPFromSSH(m) + if IP, err = RequestIPFromSSH(m); err != nil { + if B2D.Verbose { + fmt.Printf("Error getting IP via SSH: %s\n", err) + } + } } if IP != "" { - errf("\nThe VM's Host only interface IP address is: ") - fmt.Printf("%s", IP) - errf("\n\n") - } else { - errf("\nFailed to get VM Host only IP address.\n") - errf("\tWas the VM initilized using boot2docker?\n") - } - return 0 -} - -func RequestIPFromSSH(m *vbx.Machine) string { - // fall back to using the NAT port forwarded ssh - out, err := cmd(B2D.SSH, - "-v", // please leave in - this seems to improve the chance of success - "-o", "StrictHostKeyChecking=no", - "-o", "UserKnownHostsFile=/dev/null", - "-p", fmt.Sprintf("%d", m.SSHPort), - "-i", B2D.SSHKey, - "docker@localhost", - "ip addr show dev eth1", - ) - IP := "" - if err != nil { - logf("%s", err) + fmt.Println(IP) } else { - // 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" { - IP = vals[1][:strings.Index(vals[1], "/")] - break - } - } + fmt.Fprintf(os.Stderr, "\nFailed to get VM Host only IP address.\n") + fmt.Fprintf(os.Stderr, "\tWas the VM initialized using boot2docker?\n") } - return IP + return nil } // Download the boot2docker ISO image. -func cmdDownload() int { - logf("Downloading boot2docker ISO image...") - url := "https://api.github.com/repos/boot2docker/boot2docker/releases" - tag, err := getLatestReleaseName(url) - if err != nil { - logf("Failed to get latest release: %s", err) - return 1 +func cmdDownload() error { + url := B2D.ISOURL + + // match github (enterprise) release urls: + // https://api.github.com/repos/../../relases or + // https://some.github.enterprise/api/v3/repos/../../relases + re := regexp.MustCompile("https://([^/]+)(/api/v3)?/repos/([^/]+)/([^/]+)/releases") + if matches := re.FindStringSubmatch(url); len(matches) == 5 { + tag, err := getLatestReleaseName(url) + if err != nil { + return fmt.Errorf("Failed to get latest release: %s", err) + } + host := matches[1] + org := matches[3] + repo := matches[4] + if host == "api.github.com" { + host = "github.com" + } + fmt.Printf("Latest release for %s/%s/%s is %s\n", host, org, repo, tag) + url = fmt.Sprintf("https://%s/%s/%s/releases/download/%s/boot2docker.iso", host, org, repo, tag) } - logf("Latest release is %s", tag) - url = fmt.Sprintf("https://github.com/boot2docker/boot2docker/releases/download/%s/boot2docker.iso", tag) + fmt.Println("Downloading boot2docker ISO image...") if err := download(B2D.ISO, url); err != nil { - logf("Failed to download ISO image: %s", err) - return 1 + return fmt.Errorf("Failed to download ISO image: %s", err) } - logf("Success: downloaded %s\n\tto %s", url, B2D.ISO) - return 0 + fmt.Printf("Success: downloaded %s\n\tto %s\n", url, B2D.ISO) + return nil } diff --git a/config.go b/config.go index 9f6ab33..d29b324 100644 --- a/config.go +++ b/config.go @@ -10,67 +10,41 @@ import ( "runtime" toml "github.com/BurntSushi/toml" - vbx "github.com/boot2docker/boot2docker-cli/virtualbox" + "github.com/boot2docker/boot2docker-cli/driver" flag "github.com/ogier/pflag" ) -// boot2docker config. -var B2D struct { - // NOTE: separate sections with blank lines so gofmt doesn't change - // indentation all the time. - - // Gereral flags. - Verbose bool - VBM string - - // basic config - SSH string // SSH client executable - SSHGen string // SSH keygen executable - SSHKey string // SSH key to send to the vm - VM string // virtual machine name - Dir string // boot2docker directory - ISO string // boot2docker ISO image path - VMDK string // base VMDK to use as persistent disk - DiskSize uint // VM disk image size (MB) - Memory uint // VM memory size (MB) - - // NAT network: port forwarding - SSHPort uint16 // host SSH port (forward to port 22 in VM) - DockerPort uint16 // host Docker port (forward to port 2375 in VM) - - // host-only network - HostIP net.IP - DHCPIP net.IP - NetMask net.IPMask - LowerIP net.IP - UpperIP net.IP - DHCPEnabled bool - - // Serial console pipe/socket - Serial bool - SerialFile string -} - var ( // Pattern to parse a key=value line in config profile. reFlagLine = regexp.MustCompile(`^\s*(\w+)\s*=\s*([^#;]+)`) + B2D = driver.MachineConfig{} ) -func getCfgDir(name string) (string, error) { - if b2dDir := os.Getenv("BOOT2DOCKER_DIR"); b2dDir != "" { - return b2dDir, nil - } - +func homeDir() (string, error) { dir := "" - - // *nix and MSYS Windows - if dir = os.Getenv("HOME"); dir == "" { - // Windows (if not running under MSYS) + if runtime.GOOS == "windows" { dir = os.Getenv("USERPROFILE") + } else { + dir = os.Getenv("HOME") } if _, err := os.Stat(dir); err != nil { return "", err } + + return dir, nil +} + +func cfgDir(name string) (string, error) { + if name == ".boot2docker" { + if b2dDir := os.Getenv("BOOT2DOCKER_DIR"); b2dDir != "" { + return b2dDir, nil + } + } + + dir, err := homeDir() + if err != nil { + return "", err + } dir = filepath.Join(dir, name) if err := os.MkdirAll(dir, 0755); err != nil { return "", err @@ -78,7 +52,7 @@ func getCfgDir(name string) (string, error) { return dir, nil } -func getCfgFilename(dir string) string { +func cfgFilename(dir string) string { filename := os.Getenv("BOOT2DOCKER_PROFILE") if filename == "" { filename = filepath.Join(dir, "profile") @@ -100,7 +74,7 @@ func printConfig() string { // Read configuration from both profile and flags. Flags override profile. func config() (*flag.FlagSet, error) { - dir, err := getCfgDir(".boot2docker") + dir, err := cfgDir(".boot2docker") if err != nil { return nil, fmt.Errorf("failed to get boot2docker directory: %s", err) } @@ -108,33 +82,42 @@ func config() (*flag.FlagSet, error) { flags := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) flags.Usage = func() { usageLong(flags) } + driver.ConfigFlags(&B2D, flags) + + // Add the generic flags + flags.StringVar(&B2D.VM, "vm", "boot2docker-vm", "virtual machine name.") // removed for now, requires re-parsing a new config file which is too messy //flags.StringVarP(&B2D.Dir, "dir", "d", dir, "boot2docker config directory.") B2D.Dir = dir + flags.StringVar(&B2D.ISOURL, "iso-url", "https://api.github.com/repos/boot2docker/boot2docker/releases", "source URL to provision the boot2docker ISO image.") flags.StringVar(&B2D.ISO, "iso", filepath.Join(dir, "boot2docker.iso"), "path to boot2docker ISO image.") - flags.StringVar(&B2D.VMDK, "basevmdk", "", "Path to VMDK to use as base for persistent partition") - vbm := "VBoxManage" - if runtime.GOOS == "windows" { - p := "C:\\Program Files\\Oracle\\VirtualBox" - if t := os.Getenv("VBOX_INSTALL_PATH"); t != "" { - p = t - } else if t = os.Getenv("VBOX_MSI_INSTALL_PATH"); t != "" { - p = t - } - vbm = filepath.Join(p, "VBoxManage.exe") - } - flags.StringVar(&B2D.VBM, "vbm", vbm, "path to VirtualBox management utility.") + + // clobber (overwrite client binary) by default on OSX. it's more likely that + // users have installed through package manager on Linux, and if so, they should + // upgrade that way. + flags.BoolVar(&B2D.Clobber, "clobber", (runtime.GOOS == "darwin"), "overwrite Docker client binary on boot2docker upgrade") + + flags.BoolVar(&B2D.ForceUpgradeDownload, "force-upgrade-download", false, "always download on boot2docker upgrade, never skip") + + // Sven disabled this, as it is broken - if I user with a fresh computer downloads + // just the boot2docker-cli, and then runs `boot2docker --init ip`, we create a vm + // which cannot run, because it fails to have have the boot2docker.iso and the ssh keys + B2D.Init = false + //flags.BoolVarP(&B2D.Init, "init", "i", false, "auto initialize vm instance.") + flags.BoolVarP(&B2D.Verbose, "verbose", "v", false, "display verbose command invocations.") + flags.StringVar(&B2D.Driver, "driver", "virtualbox", "hypervisor driver.") flags.StringVar(&B2D.SSH, "ssh", "ssh", "path to SSH client utility.") flags.StringVar(&B2D.SSHGen, "ssh-keygen", "ssh-keygen", "path to ssh-keygen utility.") - sshdir, _ := getCfgDir(".ssh") + sshdir, _ := cfgDir(".ssh") flags.StringVar(&B2D.SSHKey, "sshkey", filepath.Join(sshdir, "id_boot2docker"), "path to SSH key to use.") flags.UintVarP(&B2D.DiskSize, "disksize", "s", 20000, "boot2docker disk image size (in MB).") flags.UintVarP(&B2D.Memory, "memory", "m", 2048, "virtual machine memory size (in MB).") + flags.UintVarP(&B2D.CPUs, "cpus", "c", uint(runtime.NumCPU()), "number of CPUs for boot2docker.") flags.Uint16Var(&B2D.SSHPort, "sshport", 2022, "host SSH port (forward to port 22 in VM).") - flags.Uint16Var(&B2D.DockerPort, "dockerport", 2375, "host Docker port (forward to port 2375 in VM).") + flags.Uint16Var(&B2D.DockerPort, "dockerport", 0, "host Docker port (forward to port 2376 in VM). (deprecated - use with care)") flags.IPVar(&B2D.HostIP, "hostip", net.ParseIP("192.168.59.3"), "VirtualBox host-only network IP address.") flags.IPMaskVar(&B2D.NetMask, "netmask", flag.ParseIPv4Mask("255.255.255.0"), "VirtualBox host-only network mask.") flags.BoolVar(&B2D.DHCPEnabled, "dhcp", true, "enable VirtualBox host-only network DHCP.") @@ -142,6 +125,9 @@ func config() (*flag.FlagSet, error) { flags.IPVar(&B2D.LowerIP, "lowerip", net.ParseIP("192.168.59.103"), "VirtualBox host-only network DHCP lower bound.") flags.IPVar(&B2D.UpperIP, "upperip", net.ParseIP("192.168.59.254"), "VirtualBox host-only network DHCP upper bound.") + flags.IntVar(&B2D.Waittime, "waittime", 300, "Time in milliseconds to wait between port knocking retries during 'start'") + flags.IntVar(&B2D.Retries, "retries", 75, "number of port knocking retries during 'start'") + if runtime.GOOS != "windows" { //SerialFile ~~ filepath.Join(dir, B2D.vm+".sock") flags.StringVar(&B2D.SerialFile, "serialfile", "", "path to the serial socket/pipe.") @@ -155,67 +141,71 @@ func config() (*flag.FlagSet, error) { return nil, err } // Over-ride from the profile file - filename := getCfgFilename(B2D.Dir) + filename := cfgFilename(B2D.Dir) if _, err := os.Lstat(filename); err == nil { if _, err := toml.DecodeFile(filename, &B2D); err != nil { return nil, err } } + if B2D.SerialFile == "" { + if runtime.GOOS == "windows" { + //SerialFile ~~ filepath.Join(dir, B2D.vm+".sock") + B2D.SerialFile = `\\.\pipe\` + B2D.VM + } else { + B2D.SerialFile = filepath.Join(dir, B2D.VM+".sock") + } + } // for cmd==ssh only: // only pass the params up to and including the `ssh` command - after that, // there might be other -flags that are destined for the ssh cmd - i := 1 - for i < len(os.Args) && os.Args[i-1] != "ssh" { - i++ + sshIdx := 1 + for sshIdx < len(os.Args) && os.Args[sshIdx-1] != "ssh" { + sshIdx++ } // Command-line overrides profile config. - if err := flags.Parse(os.Args[1:i]); err != nil { + if err := flags.Parse(os.Args[1:sshIdx]); err != nil { return nil, err } - vbx.Verbose = B2D.Verbose - vbx.VBM = B2D.VBM + leftovers := flags.Args() - if B2D.SerialFile == "" { - if runtime.GOOS == "windows" { - //SerialFile ~~ filepath.Join(dir, B2D.vm+".sock") - B2D.SerialFile = `\\.\pipe\` + B2D.VM - } else { - B2D.SerialFile = filepath.Join(dir, B2D.VM+".sock") - } + if B2D.Verbose || (len(leftovers) > 0 && leftovers[0] == "version") { + fmt.Printf("Boot2Docker-cli version: %s\nGit commit: %s\n", Version, GitSHA) } return flags, nil } func usageShort() { - errf("Usage: %s [] {help|init|up|ssh|save|down|poweroff|reset|restart|config|status|info|ip|delete|download|version} []\n", os.Args[0]) - + binName := filepath.Base(os.Args[0]) + fmt.Fprintf(os.Stderr, "Usage: %s [] {help|init|up|ssh|save|down|poweroff|reset|restart|config|status|info|ip|shellinit|delete|download|upgrade|version} []\n", binName) } func usageLong(flags *flag.FlagSet) { // NOTE: the help message uses spaces, not tabs for indentation! - errf(`Usage: %s [] [] + fmt.Fprintf(os.Stderr, `Usage: %s [] [] -boot2docker management utility. +Boot2Docker management utility. Commands: - init Create a new boot2docker VM. - up|start|boot Start VM from any states. - ssh [ssh-command] Login to VM via SSH. - save|suspend Suspend VM and save state to disk. - down|stop|halt Gracefully shutdown the VM. - restart Gracefully reboot the VM. - poweroff Forcefully power off the VM (might corrupt disk image). - reset Forcefully power cycle the VM (might corrupt disk image). - delete Delete boot2docker VM and its disk image. - config|cfg Show selected profile file settings. - info Display detailed information of VM. - ip Display the IP address of the VM's Host-only network. - status Display current state of VM. - download Download boot2docker ISO image. - version Display version information. + init Create a new Boot2Docker VM. + up|start|boot Start VM from any states. + ssh [ssh-command] Login to VM via SSH. + save|suspend Suspend VM and save state to disk. + down|stop|halt Gracefully shutdown the VM. + restart Gracefully reboot the VM. + poweroff Forcefully power off the VM (may corrupt disk image). + reset Forcefully power cycle the VM (may corrupt disk image). + delete|destroy Delete Boot2Docker VM and its disk image. + config|cfg Show selected profile file settings. + info Display detailed information of VM. + ip Display the IP address of the VM's Host-only network. + shellinit Display the shell commands to set up the Docker client. + status Display current state of VM. + download Download Boot2Docker ISO image. + upgrade Upgrade the Boot2Docker ISO image (restart if running). + version Display version information. Options: `, os.Args[0]) diff --git a/docs/images/bad_host.png b/docs/images/bad_host.png new file mode 100644 index 0000000..cdc78de Binary files /dev/null and b/docs/images/bad_host.png differ diff --git a/docs/images/cool_view.png b/docs/images/cool_view.png new file mode 100644 index 0000000..8eebf21 Binary files /dev/null and b/docs/images/cool_view.png differ diff --git a/docs/images/good_host.png b/docs/images/good_host.png new file mode 100644 index 0000000..58b9783 Binary files /dev/null and b/docs/images/good_host.png differ diff --git a/docs/images/kitematic.png b/docs/images/kitematic.png new file mode 100644 index 0000000..5bb221c Binary files /dev/null and b/docs/images/kitematic.png differ diff --git a/docs/images/newsite_view.png b/docs/images/newsite_view.png new file mode 100644 index 0000000..27b6b1a Binary files /dev/null and b/docs/images/newsite_view.png differ diff --git a/docs/images/windows-boot2docker-cmd.png b/docs/images/windows-boot2docker-cmd.png new file mode 100644 index 0000000..09e3206 Binary files /dev/null and b/docs/images/windows-boot2docker-cmd.png differ diff --git a/docs/images/windows-boot2docker-powershell.png b/docs/images/windows-boot2docker-powershell.png new file mode 100644 index 0000000..b1ef896 Binary files /dev/null and b/docs/images/windows-boot2docker-powershell.png differ diff --git a/docs/images/windows-boot2docker-start.png b/docs/images/windows-boot2docker-start.png new file mode 100644 index 0000000..5e3563b Binary files /dev/null and b/docs/images/windows-boot2docker-start.png differ diff --git a/docs/images/windows-installer.png b/docs/images/windows-installer.png new file mode 100644 index 0000000..7580ed6 Binary files /dev/null and b/docs/images/windows-installer.png differ diff --git a/docs/mac.md b/docs/mac.md new file mode 100644 index 0000000..ee5fcaa --- /dev/null +++ b/docs/mac.md @@ -0,0 +1,358 @@ + + +# Mac OS X + +You can install Docker using Boot2Docker to run `docker` commands at your command-line. +Choose this installation if you are familiar with the command-line or plan to +contribute to the Docker project on GitHub. + +[Download Kitematic](https://kitematic.com/download) + +Alternatively, you may want to try Kitematic, an application that lets you set up Docker and +run containers using a graphical user interface (GUI). + +## Command-line Docker with Boot2Docker + +Because the Docker daemon uses Linux-specific kernel features, you can't run +Docker natively in OS X. Instead, you must install the Boot2Docker application. +The application includes a VirtualBox Virtual Machine (VM), Docker itself, and the +Boot2Docker management tool. + +The Boot2Docker management tool is a lightweight Linux virtual machine made +specifically to run the Docker daemon on Mac OS X. The VirtualBox VM runs +completely from RAM, is a small ~24MB download, and boots in approximately 5s. + +**Requirements** + +Your Mac must be running OS X 10.6 "Snow Leopard" or newer to run Boot2Docker. + +### Learn the key concepts before installing + +In a Docker installation on Linux, your machine is both the localhost and the +Docker host. In networking, localhost means your computer. The Docker host is +the machine on which the containers run. + +On a typical Linux installation, the Docker client, the Docker daemon, and any +containers run directly on your localhost. This means you can address ports on a +Docker container using standard localhost addressing such as `localhost:8000` or +`0.0.0.0:8376`. + +![Linux Architecture Diagram](/installation/images/linux_docker_host.svg) + +In an OS X installation, the `docker` daemon is running inside a Linux virtual +machine provided by Boot2Docker. + +![OSX Architecture Diagram](/installation/images/mac_docker_host.svg) + +In OS X, the Docker host address is the address of the Linux VM. +When you start the `boot2docker` process, the VM is assigned an IP address. Under +`boot2docker` ports on a container map to ports on the VM. To see this in +practice, work through the exercises on this page. + + +### Installation + +1. Go to the [boot2docker/osx-installer ]( + https://github.com/boot2docker/osx-installer/releases/latest) release page. + +4. Download Boot2Docker by clicking `Boot2Docker-x.x.x.pkg` in the "Downloads" + section. + +3. Install Boot2Docker by double-clicking the package. + + The installer places Boot2Docker and VirtualBox in your "Applications" folder. + +The installation places the `docker` and `boot2docker` binaries in your +`/usr/local/bin` directory. + + +## Start the Boot2Docker Application + +To run a Docker container, you first start the `boot2docker` VM and then issue +`docker` commands to create, load, and manage containers. You can launch +`boot2docker` from your Applications folder or from the command line. + +> **NOTE**: Boot2Docker is designed as a development tool. You should not use +> it in production environments. + +### From the Applications folder + +When you launch the "Boot2Docker" application from your "Applications" folder, the +application: + +* opens a terminal window + +* creates a $HOME/.boot2docker directory + +* creates a VirtualBox ISO and certs + +* starts a VirtualBox VM running the `docker` daemon + +Once the launch completes, you can run `docker` commands. A good way to verify +your setup succeeded is to run the `hello-world` container. + + $ docker run hello-world + Unable to find image 'hello-world:latest' locally + 511136ea3c5a: Pull complete + 31cbccb51277: Pull complete + e45a5af57b00: Pull complete + hello-world:latest: The image you are pulling has been verified. + Important: image verification is a tech preview feature and should not be + relied on to provide security. + Status: Downloaded newer image for hello-world:latest + Hello from Docker. + This message shows that your installation appears to be working correctly. + + To generate this message, Docker took the following steps: + 1. The Docker client contacted the Docker daemon. + 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. + (Assuming it was not already locally available.) + 3. The Docker daemon created a new container from that image which runs the + executable that produces the output you are currently reading. + 4. The Docker daemon streamed that output to the Docker client, which sent it + to your terminal. + + To try something more ambitious, you can run an Ubuntu container with: + $ docker run -it ubuntu bash + + For more examples and ideas, visit: + http://docs.docker.com/userguide/ + + +A more typical way to start and stop `boot2docker` is using the command line. + +### From your command line + +Initialize and run `boot2docker` from the command line, do the following: + +1. Create a new Boot2Docker VM. + + $ boot2docker init + + This creates a new virtual machine. You only need to run this command once. + +2. Start the `boot2docker` VM. + + $ boot2docker start + +3. Display the environment variables for the Docker client. + + $ boot2docker shellinit + Writing /Users/mary/.boot2docker/certs/boot2docker-vm/ca.pem + Writing /Users/mary/.boot2docker/certs/boot2docker-vm/cert.pem + Writing /Users/mary/.boot2docker/certs/boot2docker-vm/key.pem + export DOCKER_HOST=tcp://192.168.59.103:2376 + export DOCKER_CERT_PATH=/Users/mary/.boot2docker/certs/boot2docker-vm + export DOCKER_TLS_VERIFY=1 + + The specific paths and address on your machine will be different. + +4. To set the environment variables in your shell do the following: + + $ eval "$(boot2docker shellinit)" + + You can also set them manually by using the `export` commands `boot2docker` + returns. + +5. Run the `hello-world` container to verify your setup. + + $ docker run hello-world + + +## Basic Boot2Docker exercises + +At this point, you should have `boot2docker` running and the `docker` client +environment initialized. To verify this, run the following commands: + + $ boot2docker status + $ docker version + +Work through this section to try some practical container tasks using `boot2docker` VM. + +### Access container ports + +1. Start an NGINX container on the DOCKER_HOST. + + $ docker run -d -P --name web nginx + + Normally, the `docker run` commands starts a container, runs it, and then + exits. The `-d` flag keeps the container running in the background + after the `docker run` command completes. The `-P` flag publishes exposed ports from the + container to your local host; this lets you access them from your Mac. + +2. Display your running container with `docker ps` command + + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 5fb65ff765e9 nginx:latest "nginx -g 'daemon of 3 minutes ago Up 3 minutes 0.0.0.0:49156->443/tcp, 0.0.0.0:49157->80/tcp web + + At this point, you can see `nginx` is running as a daemon. + +3. View just the container's ports. + + $ docker port web + 443/tcp -> 0.0.0.0:49156 + 80/tcp -> 0.0.0.0:49157 + + This tells you that the `web` container's port `80` is mapped to port + `49157` on your Docker host. + +4. Enter the `http://localhost:49157` address (`localhost` is `0.0.0.0`) in your browser: + + ![Bad Address](/installation/images/bad_host.png) + + This didn't work. The reason it doesn't work is your `DOCKER_HOST` address is + not the localhost address (0.0.0.0) but is instead the address of the + `boot2docker` VM. + +5. Get the address of the `boot2docker` VM. + + $ boot2docker ip + 192.168.59.103 + +6. Enter the `http://192.168.59.103:49157` address in your browser: + + ![Correct Addressing](/installation/images/good_host.png) + + Success! + +7. To stop and then remove your running `nginx` container, do the following: + + $ docker stop web + $ docker rm web + +### Mount a volume on the container + +When you start `boot2docker`, it automatically shares your `/Users` directory +with the VM. You can use this share point to mount directories onto your container. +The next exercise demonstrates how to do this. + +1. Change to your user `$HOME` directory. + + $ cd $HOME + +2. Make a new `site` directory. + + $ mkdir site + +3. Change into the `site` directory. + + $ cd site + +4. Create a new `index.html` file. + + $ echo "my new site" > index.html + +5. Start a new `nginx` container and replace the `html` folder with your `site` directory. + + $ docker run -d -P -v $HOME/site:/usr/share/nginx/html --name mysite nginx + +6. Get the `mysite` container's port. + + $ docker port mysite + 80/tcp -> 0.0.0.0:49166 + 443/tcp -> 0.0.0.0:49165 + +7. Open the site in a browser: + + ![My site page](/installation/images/newsite_view.png) + +8. Try adding a page to your `$HOME/site` in real time. + + $ echo "This is cool" > cool.html + +9. Open the new page in the browser. + + ![Cool page](/installation/images/cool_view.png) + +9. Stop and then remove your running `mysite` container. + + $ docker stop mysite + $ docker rm mysite + +## Upgrade Boot2Docker + +If you running Boot2Docker 1.4.1 or greater, you can upgrade Boot2Docker from +the command line. If you are running an older version, you should use the +package provided by the `boot2docker` repository. + +### From the command line + +To upgrade from 1.4.1 or greater, you can do this: + +1. Open a terminal on your local machine. + +2. Stop the `boot2docker` application. + + $ boot2docker stop + +3. Run the upgrade command. + + $ boot2docker upgrade + + +### Use the installer + +To upgrade any version of Boot2Docker, do this: + +1. Open a terminal on your local machine. + +2. Stop the `boot2docker` application. + + $ boot2docker stop + +3. Go to the [boot2docker/osx-installer ]( + https://github.com/boot2docker/osx-installer/releases/latest) release page. + +4. Download Boot2Docker by clicking `Boot2Docker-x.x.x.pkg` in the "Downloads" + section. + +2. Install Boot2Docker by double-clicking the package. + + The installer places Boot2Docker in your "Applications" folder. + + +## Uninstallation + +1. Go to the [boot2docker/osx-installer ]( + https://github.com/boot2docker/osx-installer/releases/latest) release page. + +2. Download the source code by clicking `Source code (zip)` or + `Source code (tar.gz)` in the "Downloads" section. + +3. Extract the source code. + +4. Open a terminal on your local machine. + +5. Change to the directory where you extracted the source code: + + $ cd + +6. Make sure the uninstall.sh script is executable: + + $ chmod +x uninstall.sh + +7. Run the uninstall.sh script: + + $ ./uninstall.sh + + +## Learning more and acknowledgement + +Use `boot2docker help` to list the full command line reference. For more +information about using SSH or SCP to access the Boot2Docker VM, see the README +at [Boot2Docker repository](https://github.com/boot2docker/boot2docker). + +Thanks to Chris Jones whose [blog](http://viget.com/extend/how-to-use-docker-on-os-x-the-missing-guide) +inspired me to redo this page. + +Continue with the [Docker User Guide](/userguide). diff --git a/docs/windows.md b/docs/windows.md new file mode 100644 index 0000000..974e687 --- /dev/null +++ b/docs/windows.md @@ -0,0 +1,172 @@ + + +# Windows +> **Note:** +> Docker has been tested on Windows 7 and 8.1; it may also run on older versions. +> Your processor needs to support hardware virtualization. + +The Docker Engine uses Linux-specific kernel features, so to run it on Windows +we need to use a lightweight virtual machine (VM). You use the **Windows Docker +Client** to control the virtualized Docker Engine to build, run, and manage +Docker containers. + +To make this process easier, we've designed a helper application called +[Boot2Docker](https://github.com/boot2docker/boot2docker) creates a Linux virtual +machine on Windows to run Docker on a Linux operating system. + +Although you will be using Windows Docker client, the docker engine hosting the +containers will still be running on Linux. Until the Docker engine for Windows +is developed, you can launch only Linux containers from your Windows machine. + +![Windows Architecture Diagram](/installation/images/win_docker_host.svg) + +## Demonstration + + + +## Installation + +1. Download the latest release of the + [Docker for Windows Installer](https://github.com/boot2docker/windows-installer/releases/latest). +2. Run the installer, which will install Docker Client for Windows, VirtualBox, + Git for Windows (MSYS-git), the boot2docker Linux ISO, and the Boot2Docker + management tool. + ![](/installation/images/windows-installer.png) +3. Run the **Boot2Docker Start** shortcut from your Desktop or “Program Files → + Boot2Docker for Windows”. + The Start script will ask you to enter an ssh key passphrase - the simplest + (but least secure) is to just hit [Enter]. + +4. The **Boot2Docker Start** will start a unix shell already configured to manage + Docker running inside the virtual machine. Run `docker version` to see + if it is working correctly: + +![](/installation/images/windows-boot2docker-start.png) + +## Running Docker + +> **Note:** if you are using a remote Docker daemon, such as Boot2Docker, +> then _do not_ type the `sudo` before the `docker` commands shown in the +> documentation's examples. + +**Boot2Docker Start** will automatically start a shell with environment variables +correctly set so you can start using Docker right away: + +Let's try the `hello-world` example image. Run + + $ docker run hello-world + +This should download the very small `hello-world` image and print a +`Hello from Docker.` message. + +## Using Docker from Windows Command Line Prompt (cmd.exe) + +Launch a Windows Command Line Prompt (cmd.exe). + +Boot2Docker command requires `ssh.exe` to be in the PATH, therefore we need to +include `bin` folder of the Git installation (which has ssh.exe) to the `%PATH%` +environment variable by running: + + set PATH=%PATH%;"c:\Program Files (x86)\Git\bin" + +and then we can run the `boot2docker start` command to start the Boot2Docker VM. +(Run `boot2docker init` command if you get an error saying machine does not +exist.) Then copy the instructions for cmd.exe to set the environment variables +to your console window and you are ready to run docker commands such as +`docker ps`: + +![](/installation/images/windows-boot2docker-cmd.png) + +## Using Docker from PowerShell + +Launch a PowerShell window, then you need to add `ssh.exe` to your PATH: + + $Env:Path = "${Env:Path};c:\Program Files (x86)\Git\bin" + +and after running `boot2docker start` command it will print PowerShell commands +to set the environment variables to connect Docker running inside VM. Run these +commands and you are ready to run docker commands such as `docker ps`: + +![](/installation/images/windows-boot2docker-powershell.png) + +> NOTE: You can alternatively run `boot2docker shellinit | Invoke-Expression` +> command to set the environment variables instead of copying and pasting on +> PowerShell. + +# Further Details + +The Boot2Docker management tool provides several commands: + + $ boot2docker + Usage: boot2docker.exe [] {help|init|up|ssh|save|down|poweroff|reset|restart|config|status|info|ip|shellinit|delete|download|upgrade|version} [] + +## Upgrading + +1. Download the latest release of the [Docker for Windows Installer]( + https://github.com/boot2docker/windows-installer/releases/latest) + +2. Run the installer, which will update the Boot2Docker management tool. + +3. To upgrade your existing virtual machine, open a terminal and run: + + boot2docker stop + boot2docker download + boot2docker start + +## Container port redirection + +If you are curious, the username for the boot2docker default user is `docker` +and the password is `tcuser`. + +The latest version of `boot2docker` sets up a host only network adaptor which +provides access to the container's ports. + +If you run a container with an exposed port: + + docker run --rm -i -t -p 80:80 nginx + +Then you should be able to access that nginx server using the IP address reported +to you using: + + boot2docker ip + +Typically, it is 192.168.59.103, but it could get changed by Virtualbox's DHCP +implementation. + +For further information or to report issues, please see the [Boot2Docker site](http://boot2docker.io) + +## Login with PUTTY instead of using the CMD + +Boot2Docker generates and uses the public/private key pair in your `%USERPROFILE%\.ssh` +directory so to log in you need to use the private key from this same directory. + +The private key needs to be converted into the format PuTTY uses. + +You can do this with +[puttygen](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html): + +- Open `puttygen.exe` and load ("File"->"Load" menu) the private key from + `%USERPROFILE%\.ssh\id_boot2docker` +- then click: "Save Private Key". +- Then use the saved file to login with PuTTY using `docker@127.0.0.1:2022`. + +## Uninstallation + +You can uninstall Boot2Docker using Window's standard process for removing programs. +This process does not remove the `docker-install.exe` file. You must delete that file +yourself. + +## References + +If you have Docker hosts running and if you don't wish to do a +Boot2Docker installation, you can install the docker.exe using +unofficial Windows package manager Chocolately. For information +on how to do this, see [Docker package on Chocolatey](http://chocolatey.org/packages/docker). diff --git a/download.sh b/download.sh new file mode 100644 index 0000000..1137abd --- /dev/null +++ b/download.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +set -e + +# Set version to latest unless set by user +if [ -z "$VERSION" ]; then + VERSION="1.1.2" +fi +EXTENSION="" + +echo "Downloading version ${VERSION}..." + +# OS information (contains e.g. Darwin x86_64) +UNAME=`uname -a` +# Determine platform +if [[ $UNAME == *"Darwin"* ]]; then + PLATFORM="darwin" +elif [[ ($UNAME == *MINGW*) || ($UNAME == *Cygwin*) ]]; then + PLATFORM="windows" + EXTENSION=".exe" + UNAME="${PROCESSOR_ARCHITEW6432}" +else + PLATFORM="linux" +fi +# Determine architecture +if [[ ($UNAME == *x86_64*) || ($UNAME == *amd64*) || ($UNAME == *AMD64*) ]] +then + ARCH="amd64" +else + echo "Currently, there are no 32bit binaries provided." + echo "You will need to go get / go install github.com/boot2docker/boot2docker-cli." + exit 1 +fi + +# Download binary +URL="https://github.com/boot2docker/boot2docker-cli/releases/download/v${VERSION}/boot2docker-v${VERSION}-${PLATFORM}-${ARCH}${EXTENSION}" +echo "Downloading $URL" +curl -L -o "boot2docker${EXTENSION}" "$URL" + +# Make binary executable +chmod +x "boot2docker${EXTENSION}" + +echo "Done." diff --git a/driver/config.go b/driver/config.go new file mode 100644 index 0000000..b3841e5 --- /dev/null +++ b/driver/config.go @@ -0,0 +1,79 @@ +package driver + +import ( + "fmt" + "net" + + flag "github.com/ogier/pflag" +) + +// Machine config. +type MachineConfig struct { + // Gereral flags. + Init bool + Verbose bool + Driver string + + // basic config + Clobber bool + ForceUpgradeDownload bool + SSH string // SSH client executable + SSHGen string // SSH keygen executable + SSHKey string // SSH key to send to the vm + VM string // virtual machine name + Dir string // boot2docker directory + ISOURL string // Source URL to retrieve the ISO from + ISO string // boot2docker ISO image path + DiskSize uint // VM disk image size (MB) + Memory uint // VM memory size (MB) + CPUs uint // Number of CPUs + + // NAT network: port forwarding + SSHPort uint16 // host SSH port (forward to port 22 in VM) + DockerPort uint16 // host Docker port (forward to port 2376 in VM) + + // host-only network + HostIP net.IP + DHCPIP net.IP + NetMask net.IPMask + LowerIP net.IP + UpperIP net.IP + DHCPEnabled bool + + // Serial console pipe/socket + Serial bool + SerialFile string + + // boot2docker init retry settings + Waittime int + Retries int + + DriverCfg map[string]interface{} +} + +type ConfigFunc func(B2D *MachineConfig, flags *flag.FlagSet) error + +var configs map[string]ConfigFunc // optional map of driver ConfigFunc + +func init() { + configs = make(map[string]ConfigFunc) +} + +// optional - allows a driver to add its own commandline parameters +func RegisterConfig(driver string, configFunc ConfigFunc) error { + if _, exists := configs[driver]; exists { + return fmt.Errorf("Driver already registered %s", driver) + } + configs[driver] = configFunc + + return nil +} + +func ConfigFlags(B2D *MachineConfig, flags *flag.FlagSet) error { + for _, configFunc := range configs { + if err := configFunc(B2D, flags); err != nil { + return err + } + } + return nil +} diff --git a/driver/dhcp.go b/driver/dhcp.go new file mode 100644 index 0000000..f6b6316 --- /dev/null +++ b/driver/dhcp.go @@ -0,0 +1,12 @@ +package driver + +import "net" + +// DHCP server info. +type DHCP struct { + NetworkName string + IPv4 net.IPNet + LowerIP net.IP + UpperIP net.IP + Enabled bool +} diff --git a/driver/driver.go b/driver/driver.go new file mode 100644 index 0000000..1efa0b9 --- /dev/null +++ b/driver/driver.go @@ -0,0 +1,78 @@ +package driver + +import ( + "errors" + "fmt" +) + +type InitFunc func(i *MachineConfig) (Machine, error) + +type MachineState string + +const ( + // Known ports + SSHPort = 22 + DockerPort = 2376 + + // VM states + Poweroff = MachineState("poweroff") + Running = MachineState("running") + Paused = MachineState("paused") + Saved = MachineState("saved") + Aborted = MachineState("aborted") +) + +// Machine represents a virtual machine instance +type Machine interface { + Start() error + Save() error + Pause() error + Stop() error + Refresh() error + Poweroff() error + Restart() error + Reset() error + Delete() error + Modify() error + AddNATPF(n int, name string, rule PFRule) error + DelNATPF(n int, name string) error + SetNIC(n int, nic NIC) error + AddStorageCtl(name string, ctl StorageController) error + DelStorageCtl(name string) error + AttachStorage(ctlName string, medium StorageMedium) error + GetState() MachineState + GetName() string + GetSerialFile() string + GetDockerPort() uint + GetSSHPort() uint +} + +var ( + // All registred machines + machines map[string]InitFunc + + ErrNotSupported = errors.New("driver not supported") + ErrMachineNotExist = errors.New("machine does not exist (Did you run `boot2docker init`?)") + ErrMachineExist = errors.New("machine already exists") + ErrPrerequisites = errors.New("prerequisites for machine not satisfied (hypervisor installed?)") +) + +func init() { + machines = make(map[string]InitFunc) +} + +func Register(driver string, initFunc InitFunc) error { + if _, exists := machines[driver]; exists { + return fmt.Errorf("Driver already registered %s", driver) + } + machines[driver] = initFunc + + return nil +} + +func GetMachine(mc *MachineConfig) (Machine, error) { + if initFunc, exists := machines[mc.Driver]; exists { + return initFunc(mc) + } + return nil, ErrNotSupported +} diff --git a/virtualbox/nic.go b/driver/nic.go similarity index 97% rename from virtualbox/nic.go rename to driver/nic.go index 0569be5..d6c7364 100644 --- a/virtualbox/nic.go +++ b/driver/nic.go @@ -1,4 +1,4 @@ -package virtualbox +package driver // NIC represents a virtualized network interface card. type NIC struct { diff --git a/virtualbox/pfrule.go b/driver/pfrule.go similarity index 98% rename from virtualbox/pfrule.go rename to driver/pfrule.go index 89b9de7..18b8160 100644 --- a/virtualbox/pfrule.go +++ b/driver/pfrule.go @@ -1,4 +1,4 @@ -package virtualbox +package driver import ( "fmt" diff --git a/virtualbox/storage.go b/driver/storage.go similarity index 98% rename from virtualbox/storage.go rename to driver/storage.go index ae129f4..a229daf 100644 --- a/virtualbox/storage.go +++ b/driver/storage.go @@ -1,4 +1,4 @@ -package virtualbox +package driver // StorageController represents a virtualized storage controller. type StorageController struct { diff --git a/dummy/machine.go b/dummy/machine.go new file mode 100644 index 0000000..cfc3f1d --- /dev/null +++ b/dummy/machine.go @@ -0,0 +1,191 @@ +package dummy + +import ( + "fmt" + "os" + + "github.com/boot2docker/boot2docker-cli/driver" + flag "github.com/ogier/pflag" +) + +type DriverCfg struct { + DummyParam string // Example string for dummy driver + hiddenParam string +} + +var ( + verbose bool // Verbose mode (Local copy of B2D.Verbose). + cfg DriverCfg +) + +func init() { + if err := driver.Register("dummy", InitFunc); err != nil { + fmt.Fprintf(os.Stderr, "Failed to initialize driver. Error : %s", err.Error()) + os.Exit(1) + } + if err := driver.RegisterConfig("dummy", ConfigFlags); err != nil { + fmt.Fprintf(os.Stderr, "Failed to initialize driver config. Error : %s", err.Error()) + os.Exit(1) + } +} + +// Initialize the Machine. +func InitFunc(i *driver.MachineConfig) (driver.Machine, error) { + verbose = i.Verbose + + fmt.Printf("Init dummy %s\n", i.VM) + return &Machine{Name: i.VM, State: driver.Poweroff}, nil +} + +// Add cmdline params for this driver +func ConfigFlags(B2D *driver.MachineConfig, flags *flag.FlagSet) error { + //B2D.DriverCfg["dummy"] = cfg + flags.StringVar(&cfg.DummyParam, "no-dummy", "", "Example parameter for the dummy driver.") + + return nil +} + +// Machine information. +type Machine struct { + Name string + UUID string + State driver.MachineState + CPUs uint + Memory uint // main memory (in MB) + VRAM uint // video memory (in MB) + CfgFile string + BaseFolder string + OSType string + BootOrder []string // max 4 slots, each in {none|floppy|dvd|disk|net} + DockerPort uint + SSHPort uint + SerialFile string +} + +// Refresh reloads the machine information. +func (m *Machine) Refresh() error { + fmt.Printf("Refresh %s: %s\n", m.Name, m.State) + return nil +} + +// Start starts the machine. +func (m *Machine) Start() error { + m.State = driver.Running + fmt.Printf("Start %s: %s\n", m.Name, m.State) + return nil +} + +// Suspend suspends the machine and saves its state to disk. +func (m *Machine) Save() error { + m.State = driver.Saved + fmt.Printf("Save %s: %s\n", m.Name, m.State) + return nil +} + +// Pause pauses the execution of the machine. +func (m *Machine) Pause() error { + m.State = driver.Paused + fmt.Printf("Pause %s: %s\n", m.Name, m.State) + return nil +} + +// Stop gracefully stops the machine. +func (m *Machine) Stop() error { + m.State = driver.Poweroff + fmt.Printf("Stop %s: %s\n", m.Name, m.State) + return nil +} + +// Poweroff forcefully stops the machine. State is lost and might corrupt the disk image. +func (m *Machine) Poweroff() error { + m.State = driver.Poweroff + fmt.Printf("Poweroff %s: %s\n", m.Name, m.State) + return nil +} + +// Restart gracefully restarts the machine. +func (m *Machine) Restart() error { + m.State = driver.Running + fmt.Printf("Restart %s: %s\n", m.Name, m.State) + return nil +} + +// Reset forcefully restarts the machine. State is lost and might corrupt the disk image. +func (m *Machine) Reset() error { + m.State = driver.Running + fmt.Printf("Reset %s: %s\n", m.Name, m.State) + return nil +} + +// Get current name +func (m *Machine) GetName() string { + return m.Name +} + +// Get current state +func (m *Machine) GetState() driver.MachineState { + return m.State +} + +// Get serial file +func (m *Machine) GetSerialFile() string { + return m.SerialFile +} + +// Get Docker port +func (m *Machine) GetDockerPort() uint { + return m.DockerPort +} + +// Get SSH port +func (m *Machine) GetSSHPort() uint { + return m.SSHPort +} + +// Delete deletes the machine and associated disk images. +func (m *Machine) Delete() error { + fmt.Printf("Delete %s: %s\n", m.Name, m.State) + return nil +} + +// Modify changes the settings of the machine. +func (m *Machine) Modify() error { + fmt.Printf("Modify %s: %s\n", m.Name, m.State) + return m.Refresh() +} + +// AddNATPF adds a NAT port forarding rule to the n-th NIC with the given name. +func (m *Machine) AddNATPF(n int, name string, rule driver.PFRule) error { + fmt.Println("Add NAT PF") + return nil +} + +// DelNATPF deletes the NAT port forwarding rule with the given name from the n-th NIC. +func (m *Machine) DelNATPF(n int, name string) error { + fmt.Println("Del NAT PF") + return nil +} + +// SetNIC set the n-th NIC. +func (m *Machine) SetNIC(n int, nic driver.NIC) error { + fmt.Println("Set NIC") + return nil +} + +// AddStorageCtl adds a storage controller with the given name. +func (m *Machine) AddStorageCtl(name string, ctl driver.StorageController) error { + fmt.Println("Add storage ctl") + return nil +} + +// DelStorageCtl deletes the storage controller with the given name. +func (m *Machine) DelStorageCtl(name string) error { + fmt.Println("Del storage ctl") + return nil +} + +// AttachStorage attaches a storage medium to the named storage controller. +func (m *Machine) AttachStorage(ctlName string, medium driver.StorageMedium) error { + fmt.Println("Attach storage") + return nil +} diff --git a/main.go b/main.go index 4d67534..48416ca 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,11 @@ package main -import "os" +import ( + "fmt" + "io/ioutil" + "net/http" + "os" +) // The following vars will be injected during the build process. var ( @@ -8,6 +13,25 @@ var ( GitSHA string ) +const ( + hardcodedWarning = ` + WARNING: The 'boot2docker' command line interface is officially deprecated. + + Please switch to Docker Machine (https://docs.docker.com/machine/) ASAP. + + Docker Toolbox (https://docker.com/toolbox) is the recommended install method. +` + warningURL = "https://raw.githubusercontent.com/boot2docker/boot2docker-cli/master/DEPRECATION_WARNING" +) + +type unknownCommandError struct { + cmd string +} + +func (e unknownCommandError) Error() string { + return fmt.Sprintf("Unknown command: %s", e.cmd) +} + func main() { // os.Exit will terminate the program at the place of call without running // any deferred cleanup statements. It might cause unintended effects. To @@ -15,15 +39,20 @@ func main() { // wrapper. Be careful not to indirectly trigger os.Exit() in the program, // notably via log.Fatal() and on flag.Parse() where the default behavior // is ExitOnError. - os.Exit(run()) + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "error in run: %v\n", err) + if _, ok := err.(unknownCommandError); ok { + usageShort() + } + os.Exit(1) + } } // Run the program and return exit code. -func run() int { +func run() error { flags, err := config() if err != nil { - errf("config error: %v\n", err) - return 1 + return fmt.Errorf("config error: %v\n", err) } switch cmd := flags.Arg(0); cmd { @@ -32,8 +61,10 @@ func run() int { case "config", "cfg": return cmdConfig() case "init": + printDeprecationWarning() return cmdInit() case "up", "start", "boot", "resume": + printDeprecationWarning() return cmdUp() case "save", "suspend": return cmdSave() @@ -45,28 +76,54 @@ func run() int { return cmdRestart() case "reset": return cmdReset() - case "delete": + case "delete", "destroy": return cmdDelete() case "info": return cmdInfo() + case "shellinit", "socket": + return cmdShellInit() case "status": return cmdStatus() case "ssh": return cmdSSH() case "ip": return cmdIP() + case "upgrade": + return cmdUpgrade() case "version": - outf("Client version: %s\nGit commit: %s\n", Version, GitSHA) - return 0 + // Version is now printed by the call to config() + return nil case "help": flags.Usage() - return 0 + return nil case "": usageShort() - return 0 + return nil default: - errf("Unknown command %q\n", cmd) - usageShort() - return 1 + return unknownCommandError{cmd: cmd} + } +} + +func printDeprecationWarning() { + var ( + warning string + ) + + // Try to get the warning from the Github raw URL. If there's any + // failure along the way, e.g. network, just fall back to the default + // warning hardcoded in the source. + resp, err := http.Get(warningURL) + if err != nil || resp.StatusCode != http.StatusOK { + warning = hardcodedWarning + } else { + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + warning = hardcodedWarning + } else { + warning = string(body) + } } + + fmt.Fprintln(os.Stderr, warning) } diff --git a/util.go b/util.go index 5b1ebde..869afd6 100644 --- a/util.go +++ b/util.go @@ -1,9 +1,13 @@ package main import ( + "archive/tar" + "bufio" + "bytes" "encoding/json" "fmt" "io" + "io/ioutil" "log" "net" "net/http" @@ -13,29 +17,21 @@ import ( "regexp" "strings" "time" -) - -// fmt.Printf to stdout. Convention is to outf info intended for scripting. -func outf(f string, v ...interface{}) { - fmt.Printf(f, v...) -} -// fmt.Printf to stderr. Convention is to errf info intended for human. -func errf(f string, v ...interface{}) { - fmt.Fprintf(os.Stderr, f, v...) -} + "github.com/boot2docker/boot2docker-cli/driver" +) -// Verbose output for debugging. -func logf(fmt string, v ...interface{}) { - log.Printf(fmt, v...) -} +var ( + // We're looking to get e.g. "1.2.0" from "Docker version 1.2.0, build fa7b24f" + versionRe = regexp.MustCompile(`(\d+\.?){3}`) +) // Try if addr tcp://addr is readable for n times at wait interval. func read(addr string, n int, wait time.Duration) error { var lastErr error for i := 0; i < n; i++ { if B2D.Verbose { - logf("Connecting to tcp://%v (attempt #%d)", addr, i) + fmt.Printf("Connecting to tcp://%v (attempt #%d)\n", addr, i) } conn, err := net.DialTimeout("tcp", addr, 1*time.Second) if err != nil { @@ -114,34 +110,62 @@ func getLatestReleaseName(url string) (string, error) { defer rsp.Body.Close() var t []struct { - TagName string `json:"tag_name"` + // ".../tags" endpoints + Name string `json:"name"` + + // ".../releases" endpoints + TagName string `json:"tag_name"` + Prerelease bool `json:"prerelease"` } - if err := json.NewDecoder(rsp.Body).Decode(&t); err != nil { + body, err := ioutil.ReadAll(rsp.Body) + if err != nil { return "", err } + + if err := json.Unmarshal(body, &t); err != nil { + var e struct { + Message string + DocumentationUrl string + } + if err := json.Unmarshal(body, &e); err != nil { + return "", fmt.Errorf("Error decoding %s\nbody: %s", err, body) + } + return "", fmt.Errorf("Error getting releases: %s\n see %s", e.Message, e.DocumentationUrl) + } if len(t) == 0 { - return "", fmt.Errorf("no releases found") + return "", fmt.Errorf("no releases found at %q", url) } - return t[0].TagName, nil -} -// Convenient function to exec a command. -func cmd(name string, args ...string) (string, error) { - cmd := exec.Command(name, args...) - if B2D.Verbose { - cmd.Stderr = os.Stderr - log.Printf("executing: %v %v", name, strings.Join(args, " ")) + // Looking up by tag instead of release. + // Github API call for docker releases yields nothing, + // so we use tags API call in this case. + if strings.Contains(url, "tags") { + return t[0].Name, nil } - b, err := cmd.Output() - return string(b), err + for _, rel := range t { + if rel.Prerelease { + // skip "pre-releases" (RCs, etc) entirely + continue + } + return rel.TagName, nil + } + + return "", fmt.Errorf("no non-prerelease releases found at %q", url) } -func cmdInteractive(name string, args ...string) error { - cmd := exec.Command(name, args...) - if B2D.Verbose { - logf("executing: %v %v", name, strings.Join(args, " ")) +func getLocalClientVersion() (string, error) { + versionOutput, err := exec.Command("docker", "-v").Output() + if err != nil { + return "", err } + versionNumber := versionRe.FindString(string(versionOutput)) + + return versionNumber, nil +} + +func cmdInteractive(m driver.Machine, args ...string) error { + cmd := getSSHCommand(m, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -179,12 +203,84 @@ func reader(r io.Reader) { } } +func getSSHCommand(m driver.Machine, 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", m.GetSSHPort()), + "-i", B2D.SSHKey, + "docker@localhost", + } + + sshArgs := append(DefaultSSHArgs, args...) + cmd := exec.Command(B2D.SSH, sshArgs...) + if B2D.Verbose { + cmd.Stderr = os.Stderr + log.Printf("executing: %v %v", cmd.Path, strings.Join(cmd.Args, " ")) + } + + return cmd +} + +func RequestIPFromSSH(m driver.Machine) (string, error) { + cmd := getSSHCommand(m, "ip addr show dev eth1") + + b, err := cmd.Output() + if err != nil { + return "", err + } + out := string(b) + if B2D.Verbose { + fmt.Printf("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 RequestSocketFromSSH(m driver.Machine) (string, error) { + cmd := getSSHCommand(m, "grep tcp:// /proc/$(cat /var/run/docker.pid)/cmdline") + + b, err := cmd.Output() + if err != nil { + return "", err + } + out := string(b) + if B2D.Verbose { + fmt.Printf("SSH returned: %s\nEND SSH\n", out) + } + // Lets only use the first one - its possible to specify more than one... + lines := strings.Split(out, "\n") + tcpRE := regexp.MustCompile(`^(tcp://)(0.0.0.0)(:.*)`) + if s := tcpRE.FindStringSubmatch(lines[0]); s != nil { + IP, err := RequestIPFromSSH(m) + if err != nil { + return "", err + } + return s[1] + IP + s[3], nil + } + if !strings.HasPrefix(lines[0], "tcp://") { + return "", fmt.Errorf("Error requesting Docker Socket: %s", lines[0]) + } + return lines[0], nil +} + // use the serial port socket to ask what the VM's host only IP is -func RequestIPFromSerialPort(socket string) string { +func RequestIPFromSerialPort(socket string) (string, error) { c, err := net.Dial("unix", socket) if err != nil { - return "" + return "", err } defer c.Close() c.SetDeadline(time.Now().Add(time.Second)) @@ -199,15 +295,14 @@ func RequestIPFromSerialPort(socket string) string { for IP == "" { _, err := c.Write([]byte("ip addr show dev eth1\r")) if err != nil { - println(err) - break + return "", err } time.Sleep(1 * time.Second) buf := make([]byte, 1024) for { n, err := c.Read(buf[:]) if err != nil { - return IP + return "", err } line = line + string(buf[0:n]) fullLog += string(buf[0:n]) @@ -215,8 +310,8 @@ func RequestIPFromSerialPort(socket string) string { //go looking for the string we want, and chomp line to after the \n if i := strings.IndexAny(line, "\n"); i != -1 { // inet 10.180.1.3/16 brd 10.180.255.255 scope global wlan0 - inet := regexp.MustCompile(`^[\t ]*inet ([0-9.]*).*$`) - if ip := inet.FindStringSubmatch(line[:i]); ip != nil { + inetRE := regexp.MustCompile(`^[\t ]*inet ([0-9.]*).*$`) + if ip := inetRE.FindStringSubmatch(line[:i]); ip != nil { IP = ip[1] // clean up break @@ -230,10 +325,64 @@ func RequestIPFromSerialPort(socket string) string { } go reader(c) //give us time reader clean up - time.Sleep(1) + time.Sleep(1 * time.Second) if IP == "" && B2D.Verbose { - logf(fullLog) + fmt.Printf(fullLog) } - return IP + return IP, nil +} + +// TODO: need to add or abstract to get a Serial coms version +// RequestCertsUsingSSH requests certs using SSH. +// The assumption is that if the certs are in b2d:/home/docker/.docker +// then the daemon is using TLS. We can't assume that because there are +// certs in the local host's user dir, that the server is using them, so +// for now, make sure things are updated from the server. (for `docker shellinit`) +func RequestCertsUsingSSH(m driver.Machine) (string, error) { + cmd := getSSHCommand(m, "tar c /home/docker/.docker/*.pem") + + certDir := "" + + b, err := cmd.Output() + if err == nil { + dir, err := cfgDir(".boot2docker") + if err != nil { + return "", err + } + + certDir = filepath.Join(dir, "certs", m.GetName()) + + // Open the tar archive for reading. + r := bytes.NewReader(b) + tr := tar.NewReader(r) + + // Iterate through the files in the archive. + for { + hdr, err := tr.Next() + if err == io.EOF { + // end of tar archive + break + } + if err != nil { + return "", err + } + filename := filepath.Base(hdr.Name) + if err := os.MkdirAll(certDir, 0755); err != nil { + return "", err + } + certFile := filepath.Join(certDir, filename) + fmt.Fprintf(os.Stderr, "Writing %s\n", certFile) + f, err := os.Create(certFile) + if err != nil { + return "", err + } + w := bufio.NewWriter(f) + if _, err := io.Copy(w, tr); err != nil { + return "", err + } + w.Flush() + } + } + return certDir, nil } diff --git a/vbm.go b/vbm.go deleted file mode 100644 index e1d9b72..0000000 --- a/vbm.go +++ /dev/null @@ -1,107 +0,0 @@ -package main - -import ( - "bytes" - "io" - "os" - "path/filepath" - - vbx "github.com/boot2docker/boot2docker-cli/virtualbox" -) - -// TODO: delete the hostonlyif and dhcpserver when we delete the vm! (need to -// reference count to make sure there are no other vms relying on them) - -// Get or create the hostonly network interface -func getHostOnlyNetworkInterface() (string, error) { - // Check if the interface/dhcp exists. - nets, err := vbx.HostonlyNets() - if err != nil { - return "", err - } - - dhcps, err := vbx.DHCPs() - if err != nil { - return "", err - } - - for _, n := range nets { - if dhcp, ok := dhcps[n.NetworkName]; ok { - if dhcp.IPv4.IP.Equal(B2D.DHCPIP) && - dhcp.IPv4.Mask.String() == B2D.NetMask.String() && - dhcp.LowerIP.Equal(B2D.LowerIP) && - dhcp.UpperIP.Equal(B2D.UpperIP) && - dhcp.Enabled == B2D.DHCPEnabled { - if B2D.Verbose { - logf("Reusing existing host-only network interface %q", n.Name) - } - return n.Name, nil - } - } - } - - // No existing host-only interface found. Create a new one. - if B2D.Verbose { - logf("Creating a new host-only network interface...") - } - hostonlyNet, err := vbx.CreateHostonlyNet() - if err != nil { - return "", err - } - hostonlyNet.IPv4.IP = B2D.HostIP - hostonlyNet.IPv4.Mask = B2D.NetMask - if err := hostonlyNet.Config(); err != nil { - return "", err - } - - // Create and add a DHCP server to the host-only network - dhcp := vbx.DHCP{} - dhcp.IPv4.IP = B2D.DHCPIP - dhcp.IPv4.Mask = B2D.NetMask - dhcp.LowerIP = B2D.LowerIP - dhcp.UpperIP = B2D.UpperIP - dhcp.Enabled = true - if err := vbx.AddHostonlyDHCP(hostonlyNet.Name, dhcp); err != nil { - return "", err - } - return hostonlyNet.Name, nil -} - -// Copy disk image from given source path to destination -func copyDiskImage(dst, src string) (err error) { - // Open source disk image - srcImg, err := os.Open(src) - if err != nil { - return err - } - closeSrcImg := func() { - if ee := srcImg.Close(); ee != nil { - err = ee - } - } - defer closeSrcImg() - dstImg, err := os.Create(dst) - if err != nil { - return err - } - closeDstImg := func() { - if ee := dstImg.Close(); ee != nil { - err = ee - } - } - defer closeDstImg() - _, err = io.Copy(dstImg, srcImg) - return err -} - -// Make a boot2docker VM disk image with the given size (in MB). -func makeDiskImage(dest string, size uint, initialBytes []byte) error { - // Create the dest dir. - if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil { - return err - } - // Fill in the magic string so boot2docker VM will detect this and format - // the disk upon first boot. - raw := bytes.NewReader(initialBytes) - return vbx.MakeDiskImage(dest, size, raw) -} diff --git a/virtualbox/dhcp.go b/virtualbox/dhcp.go index d18d7a4..d686e8e 100644 --- a/virtualbox/dhcp.go +++ b/virtualbox/dhcp.go @@ -4,18 +4,11 @@ import ( "bufio" "net" "strings" -) -// DHCP server info. -type DHCP struct { - NetworkName string - IPv4 net.IPNet - LowerIP net.IP - UpperIP net.IP - Enabled bool -} + "github.com/boot2docker/boot2docker-cli/driver" +) -func addDHCP(kind, name string, d DHCP) error { +func addDHCP(kind, name string, d driver.DHCP) error { command := "modify" // On some platforms (OSX), creating a hostonlyinterface adds a default dhcpserver @@ -45,29 +38,29 @@ func addDHCP(kind, name string, d DHCP) error { } // AddInternalDHCP adds a DHCP server to an internal network. -func AddInternalDHCP(netname string, d DHCP) error { +func AddInternalDHCP(netname string, d driver.DHCP) error { return addDHCP("--netname", netname, d) } // AddHostonlyDHCP adds a DHCP server to a host-only network. -func AddHostonlyDHCP(ifname string, d DHCP) error { +func AddHostonlyDHCP(ifname string, d driver.DHCP) error { return addDHCP("--netname", "HostInterfaceNetworking-"+ifname, d) } // DHCPs gets all DHCP server settings in a map keyed by DHCP.NetworkName. -func DHCPs() (map[string]*DHCP, error) { +func DHCPs() (map[string]*driver.DHCP, error) { out, err := vbmOut("list", "dhcpservers") if err != nil { return nil, err } s := bufio.NewScanner(strings.NewReader(out)) - m := map[string]*DHCP{} - dhcp := &DHCP{} + m := map[string]*driver.DHCP{} + dhcp := &driver.DHCP{} for s.Scan() { line := s.Text() if line == "" { m[dhcp.NetworkName] = dhcp - dhcp = &DHCP{} + dhcp = &driver.DHCP{} continue } res := reColonLine.FindStringSubmatch(line) diff --git a/virtualbox/disk.go b/virtualbox/disk.go index 988dae8..d67703a 100644 --- a/virtualbox/disk.go +++ b/virtualbox/disk.go @@ -12,10 +12,10 @@ import ( func MakeDiskImage(dest string, size uint, 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) - cmd := exec.Command(VBM, "convertfromraw", "stdin", dest, + cmd := exec.Command(cfg.VBM, "convertfromraw", "stdin", dest, fmt.Sprintf("%d", sizeBytes), "--format", "VMDK") - if Verbose { + if verbose { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr } diff --git a/virtualbox/hostonlynet.go b/virtualbox/hostonlynet.go index 305c245..6e25699 100644 --- a/virtualbox/hostonlynet.go +++ b/virtualbox/hostonlynet.go @@ -100,7 +100,7 @@ func HostonlyNets() (map[string]*HostonlyNet, error) { case "IPV6Address": n.IPv6.IP = net.ParseIP(val) case "IPV6NetworkMaskPrefixLength": - l, err := strconv.ParseUint(val, 10, 7) + l, err := strconv.ParseUint(val, 10, 8) if err != nil { return nil, err } diff --git a/virtualbox/machine.go b/virtualbox/machine.go index e9324bd..acbd146 100644 --- a/virtualbox/machine.go +++ b/virtualbox/machine.go @@ -1,22 +1,21 @@ package virtualbox import ( + "archive/tar" "bufio" + "bytes" "fmt" + "io/ioutil" + "net" + "os" "path/filepath" + "runtime" "strconv" "strings" "time" -) - -type MachineState string -const ( - Poweroff = MachineState("poweroff") - Running = MachineState("running") - Paused = MachineState("paused") - Saved = MachineState("saved") - Aborted = MachineState("aborted") + "github.com/boot2docker/boot2docker-cli/driver" + flag "github.com/ogier/pflag" ) type Flag int @@ -29,7 +28,6 @@ const ( F_cpuhotplug F_pae F_longmode - F_synthcpu F_hpet F_hwvirtex F_triplefaultreset @@ -40,6 +38,111 @@ const ( F_accelerate3d ) +type DriverCfg struct { + VBM string // Path to VBoxManage utility. + VMDK string // base VMDK to use as persistent disk. + + shares shareSlice + + // see also func ConfigFlags later in this file +} + +var shareDefault string // set in ConfigFlags - this is what gets filled in for "shares" if it's empty + +var ( + verbose bool // Verbose mode (Local copy of B2D.Verbose). + cfg DriverCfg +) + +func init() { + if err := driver.Register("virtualbox", InitFunc); err != nil { + fmt.Fprintf(os.Stderr, "Failed to initialize driver. Error : %s", err.Error()) + os.Exit(1) + } + if err := driver.RegisterConfig("virtualbox", ConfigFlags); err != nil { + fmt.Fprintf(os.Stderr, "Failed to initialize driver config. Error : %s", err.Error()) + os.Exit(1) + } +} + +// Initialize the Machine. +func InitFunc(mc *driver.MachineConfig) (driver.Machine, error) { + verbose = mc.Verbose + + m, err := GetMachine(mc.VM) + if err != nil && mc.Init { + return CreateMachine(mc) + } + return m, err +} + +type shareSlice map[string]string + +const shareSliceSep = "=" + +func (s shareSlice) String() string { + var ret []string + for name, dir := range s { + ret = append(ret, fmt.Sprintf("%s%s%s", dir, shareSliceSep, name)) + } + return fmt.Sprintf("[%s]", strings.Join(ret, " ")) +} + +func (s *shareSlice) Set(shareDir string) error { + var shareName string + if i := strings.Index(shareDir, shareSliceSep); i >= 0 { + shareName = shareDir[i+1:] + shareDir = shareDir[:i] + } + 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 + } + if *s == nil { + *s = shareSlice{} + } + (*s)[shareName] = shareDir + return nil +} + +// Add cmdline params for this driver +func ConfigFlags(B2D *driver.MachineConfig, flags *flag.FlagSet) error { + //B2D.DriverCfg["virtualbox"] = cfg + + flags.StringVar(&cfg.VMDK, "basevmdk", "", "Path to VMDK to use as base for persistent partition") + + cfg.VBM = "VBoxManage" + if runtime.GOOS == "windows" { + p := "C:\\Program Files\\Oracle\\VirtualBox" + if t := os.Getenv("VBOX_INSTALL_PATH"); t != "" { + p = t + } else if t = os.Getenv("VBOX_MSI_INSTALL_PATH"); t != "" { + p = t + } + cfg.VBM = filepath.Join(p, "VBoxManage.exe") + } + flags.StringVar(&cfg.VBM, "vbm", cfg.VBM, "path to VirtualBox management utility.") + + // TODO once boot2docker improves, replace this all with homeDir() from config.go so we only share the current user's HOME by default + shareDefault = "disable" + switch runtime.GOOS { + case "darwin": + shareDefault = "/Users" + shareSliceSep + "Users" + case "windows": + shareDefault = "C:\\Users" + shareSliceSep + "c/Users" + } + + var defaultText string + if shareDefault != "disable" { + defaultText = "(defaults to '" + shareDefault + "' if no shares are specified; use 'disable' to explicitly prevent any shares from being created) " + } + flags.Var(&cfg.shares, "vbox-share", fmt.Sprintf("%sList of directories to share during 'up|start|boot' via VirtualBox Guest Additions, with optional labels", defaultText)) + + return nil +} + // Convert bool to "on"/"off" func bool2string(b bool) string { if b { @@ -57,7 +160,8 @@ func (f Flag) Get(o Flag) string { type Machine struct { Name string UUID string - State MachineState + Iso string + State driver.MachineState CPUs uint Memory uint // main memory (in MB) VRAM uint // video memory (in MB) @@ -88,13 +192,18 @@ func (m *Machine) Refresh() error { // Start starts the machine. func (m *Machine) Start() error { switch m.State { - case Paused: + case driver.Paused: return vbm("controlvm", m.Name, "resume") - case Poweroff, Saved, Aborted: + case driver.Poweroff, driver.Aborted: + if err := m.setUpShares(); err != nil { + return err + } + fallthrough + case driver.Saved: return vbm("startvm", m.Name, "--type", "headless") } if err := m.Refresh(); err == nil { - if m.State != Running { + if m.State != driver.Running { return fmt.Errorf("Failed to start", m.Name) } } @@ -104,11 +213,11 @@ func (m *Machine) Start() error { // Suspend suspends the machine and saves its state to disk. func (m *Machine) Save() error { switch m.State { - case Paused: + case driver.Paused: if err := m.Start(); err != nil { return err } - case Poweroff, Aborted, Saved: + case driver.Poweroff, driver.Aborted, driver.Saved: return nil } return vbm("controlvm", m.Name, "savestate") @@ -117,7 +226,7 @@ func (m *Machine) Save() error { // Pause pauses the execution of the machine. func (m *Machine) Pause() error { switch m.State { - case Paused, Poweroff, Aborted, Saved: + case driver.Paused, driver.Poweroff, driver.Aborted, driver.Saved: return nil } return vbm("controlvm", m.Name, "pause") @@ -126,15 +235,16 @@ func (m *Machine) Pause() error { // Stop gracefully stops the machine. func (m *Machine) Stop() error { switch m.State { - case Poweroff, Aborted, Saved: + case driver.Poweroff, driver.Aborted, driver.Saved: return nil - case Paused: + case driver.Paused: if err := m.Start(); err != nil { return err } } - for m.State != Poweroff { // busy wait until the machine is stopped + // busy wait until the machine is stopped + for i := 0; i < 10; i++ { if err := vbm("controlvm", m.Name, "acpipowerbutton"); err != nil { return err } @@ -142,14 +252,18 @@ func (m *Machine) Stop() error { if err := m.Refresh(); err != nil { return err } + if m.State == driver.Poweroff { + return nil + } } - return nil + + return fmt.Errorf("timed out waiting for VM to stop") } // Poweroff forcefully stops the machine. State is lost and might corrupt the disk image. func (m *Machine) Poweroff() error { switch m.State { - case Poweroff, Aborted, Saved: + case driver.Poweroff, driver.Aborted, driver.Saved: return nil } return vbm("controlvm", m.Name, "poweroff") @@ -158,7 +272,7 @@ func (m *Machine) Poweroff() error { // Restart gracefully restarts the machine. func (m *Machine) Restart() error { switch m.State { - case Paused, Saved: + case driver.Paused, driver.Saved: if err := m.Start(); err != nil { return err } @@ -172,7 +286,7 @@ func (m *Machine) Restart() error { // Reset forcefully restarts the machine. State is lost and might corrupt the disk image. func (m *Machine) Reset() error { switch m.State { - case Paused, Saved: + case driver.Paused, driver.Saved: if err := m.Start(); err != nil { return err } @@ -188,12 +302,37 @@ func (m *Machine) Delete() error { return vbm("unregistervm", m.Name, "--delete") } +// Get current state +func (m *Machine) GetName() string { + return m.Name +} + +// Get current state +func (m *Machine) GetState() driver.MachineState { + return m.State +} + +// Get serial file +func (m *Machine) GetSerialFile() string { + return m.SerialFile +} + +// Get Docker port +func (m *Machine) GetDockerPort() uint { + return m.DockerPort +} + +// Get SSH port +func (m *Machine) GetSSHPort() uint { + return m.SSHPort +} + // GetMachine finds a machine by its name or UUID. func GetMachine(id string) (*Machine, error) { stdout, stderr, err := vbmOutErr("showvminfo", id, "--machinereadable") if err != nil { if reMachineNotFound.FindString(stderr) != "" { - return nil, ErrMachineNotExist + return nil, driver.ErrMachineNotExist } return nil, err } @@ -218,8 +357,10 @@ func GetMachine(id string) (*Machine, error) { m.Name = val case "UUID": m.UUID = val + case "SATA-0-0": + m.Iso = val case "VMState": - m.State = MachineState(val) + m.State = driver.MachineState(val) case "memory": n, err := strconv.ParseUint(val, 10, 32) if err != nil { @@ -241,28 +382,29 @@ func GetMachine(id string) (*Machine, error) { case "CfgFile": m.CfgFile = val m.BaseFolder = filepath.Dir(val) - case "Forwarding(0)": - // Forwarding(0)="docker,tcp,127.0.0.1,5555,," - vals := strings.Split(val, ",") - n, err := strconv.ParseUint(vals[3], 10, 32) - if err != nil { - return nil, err - } - m.DockerPort = uint(n) - case "Forwarding(1)": - // Forwarding(1)="ssh,tcp,127.0.0.1,2222,,22" - vals := strings.Split(val, ",") - n, err := strconv.ParseUint(vals[3], 10, 32) - if err != nil { - return nil, err - } - m.SSHPort = uint(n) case "uartmode1": // uartmode1="server,/home/sven/.boot2docker/boot2docker-vm.sock" vals := strings.Split(val, ",") if len(vals) >= 2 { m.SerialFile = vals[1] } + default: + if strings.HasPrefix(key, "Forwarding(") { + // "Forwarding(\d*)" are ordered by the name inside the val, not fixed order. + // Forwarding(0)="docker,tcp,127.0.0.1,5555,," + // Forwarding(1)="ssh,tcp,127.0.0.1,2222,,22" + vals := strings.Split(val, ",") + n, err := strconv.ParseUint(vals[3], 10, 32) + if err != nil { + return nil, err + } + switch vals[0] { + case "docker": + m.DockerPort = uint(n) + case "ssh": + m.SSHPort = uint(n) + } + } } } if err := s.Err(); err != nil { @@ -293,8 +435,8 @@ func ListMachines() ([]string, error) { } // CreateMachine creates a new machine. If basefolder is empty, use default. -func CreateMachine(name, basefolder string) (*Machine, error) { - if name == "" { +func CreateMachine(mc *driver.MachineConfig) (*Machine, error) { + if mc.VM == "" { return nil, fmt.Errorf("machine name is empty") } @@ -304,28 +446,190 @@ func CreateMachine(name, basefolder string) (*Machine, error) { return nil, err } for _, m := range machineNames { - if m == name { - return nil, ErrMachineExist + if m == mc.VM { + return nil, driver.ErrMachineExist } } // Create and register the machine. - args := []string{"createvm", "--name", name, "--register"} - if basefolder != "" { - args = append(args, "--basefolder", basefolder) - } + args := []string{"createvm", "--name", mc.VM, "--register"} if err := vbm(args...); err != nil { return nil, err } - m, err := GetMachine(name) + m, err := GetMachine(mc.VM) if err != nil { return nil, err } + // Configure VM for Boot2docker + SetExtra(mc.VM, "VBoxInternal/CPUM/EnableHVP", "1") + m.OSType = "Linux26_64" + if mc.CPUs > 0 { + m.CPUs = mc.CPUs + } else { + m.CPUs = uint(runtime.NumCPU()) + } + if m.CPUs > 32 { + m.CPUs = 32 + } + m.Memory = mc.Memory + m.SerialFile = mc.SerialFile + + m.Flag |= F_pae + m.Flag |= F_longmode // important: use x86-64 processor + m.Flag |= F_rtcuseutc + m.Flag |= F_acpi + m.Flag |= F_ioapic + m.Flag |= F_hpet + m.Flag |= F_hwvirtex + m.Flag |= F_vtxvpid + m.Flag |= F_largepages + m.Flag |= F_nestedpaging + + // Set VM boot order + m.BootOrder = []string{"dvd"} + if err := m.Modify(); err != nil { + return m, err + } + + // Set NIC #1 to use NAT + m.SetNIC(1, driver.NIC{Network: driver.NICNetNAT, Hardware: driver.VirtIO}) + pfRules := map[string]driver.PFRule{ + "ssh": {Proto: driver.PFTCP, HostIP: net.ParseIP("127.0.0.1"), HostPort: mc.SSHPort, GuestPort: driver.SSHPort}, + } + if mc.DockerPort > 0 { + pfRules["docker"] = driver.PFRule{Proto: driver.PFTCP, HostIP: net.ParseIP("127.0.0.1"), HostPort: mc.DockerPort, GuestPort: driver.DockerPort} + } + + for name, rule := range pfRules { + if err := m.AddNATPF(1, name, rule); err != nil { + return m, err + } + } + + hostIFName, err := getHostOnlyNetworkInterface(mc) + if err != nil { + return m, err + } + + // Set NIC #2 to use host-only + if err := m.SetNIC(2, driver.NIC{Network: driver.NICNetHostonly, Hardware: driver.VirtIO, HostonlyAdapter: hostIFName}); err != nil { + return m, err + } + + // Set VM storage + if err := m.AddStorageCtl("SATA", driver.StorageController{SysBus: driver.SysBusSATA, HostIOCache: true, Bootable: true, Ports: 4}); err != nil { + return m, err + } + + // Attach ISO image + if err := m.AttachStorage("SATA", driver.StorageMedium{Port: 0, Device: 0, DriveType: driver.DriveDVD, Medium: mc.ISO}); err != nil { + return m, err + } + + diskImg := filepath.Join(m.BaseFolder, fmt.Sprintf("%s.vmdk", mc.VM)) + if _, err := os.Stat(diskImg); err != nil { + if !os.IsNotExist(err) { + return m, err + } + + if cfg.VMDK != "" { + if err := copyDiskImage(diskImg, cfg.VMDK); err != nil { + return m, err + } + } else { + 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 m, err + } + if _, err := tw.Write([]byte(magicString)); err != nil { + return m, err + } + // .ssh/key.pub => authorized_keys + file = &tar.Header{Name: ".ssh", Typeflag: tar.TypeDir, Mode: 0700} + if err := tw.WriteHeader(file); err != nil { + return m, err + } + pubKey, err := ioutil.ReadFile(mc.SSHKey + ".pub") + if err != nil { + return m, err + } + file = &tar.Header{Name: ".ssh/authorized_keys", Size: int64(len(pubKey)), Mode: 0644} + if err := tw.WriteHeader(file); err != nil { + return m, err + } + if _, err := tw.Write([]byte(pubKey)); err != nil { + return m, err + } + file = &tar.Header{Name: ".ssh/authorized_keys2", Size: int64(len(pubKey)), Mode: 0644} + if err := tw.WriteHeader(file); err != nil { + return m, err + } + if _, err := tw.Write([]byte(pubKey)); err != nil { + return m, err + } + if err := tw.Close(); err != nil { + return m, err + } + + if err := makeDiskImage(diskImg, mc.DiskSize, buf.Bytes()); err != nil { + return m, err + } + if verbose { + fmt.Println("Initializing disk with ssh keys") + fmt.Printf("WRITING: %s\n-----\n", buf) + } + } + } + + if err := m.AttachStorage("SATA", driver.StorageMedium{Port: 1, Device: 0, DriveType: driver.DriveHDD, Medium: diskImg}); err != nil { + return m, err + } + return m, nil } +func (m *Machine) setUpShares() error { + // let VBoxService do nice magic automounting (when it's used) + if err := vbm("guestproperty", "set", m.Name, "/VirtualBox/GuestAdd/SharedFolders/MountPrefix", "/"); err != nil { + return err + } + if err := vbm("guestproperty", "set", m.Name, "/VirtualBox/GuestAdd/SharedFolders/MountDir", "/"); err != nil { + return err + } + + // set up some shared folders as appropriate + if len(cfg.shares) == 0 { + cfg.shares.Set(shareDefault) + } + for shareName, shareDir := range cfg.shares { + if shareDir == "disable" { + continue + } + if _, err := os.Stat(shareDir); err != nil { + return err + } + + // woo, shareDir exists! let's carry on! + if err := vbm("sharedfolder", "add", m.Name, "--name", shareName, "--hostpath", shareDir, "--automount"); err != nil { + return err + } + + // enable symlinks + if err := vbm("setextradata", m.Name, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/"+shareName, "1"); err != nil { + return err + } + } + return nil +} + // Modify changes the settings of the machine. func (m *Machine) Modify() error { args := []string{"modifyvm", m.Name, @@ -335,6 +639,13 @@ func (m *Machine) Modify() error { "--bioslogodisplaytime", "0", "--biosbootmenu", "disabled", + // the DNS Host Resolver doesn't support SRV records + // the DNS proxy has performance issues + // direct DNS pass-through doesn't support roaming laptops well + // we can't win, so let's go direct and at least get performance + "--natdnshostresolver1", "off", + "--natdnsproxy1", "off", + "--ostype", m.OSType, "--cpus", fmt.Sprintf("%d", m.CPUs), "--memory", fmt.Sprintf("%d", m.Memory), @@ -346,7 +657,6 @@ func (m *Machine) Modify() error { "--cpuhotplug", m.Flag.Get(F_cpuhotplug), "--pae", m.Flag.Get(F_pae), "--longmode", m.Flag.Get(F_longmode), - "--synthcpu", m.Flag.Get(F_synthcpu), "--hpet", m.Flag.Get(F_hpet), "--hwvirtex", m.Flag.Get(F_hwvirtex), "--triplefaultreset", m.Flag.Get(F_triplefaultreset), @@ -377,8 +687,8 @@ func (m *Machine) Modify() error { } // AddNATPF adds a NAT port forarding rule to the n-th NIC with the given name. -func (m *Machine) AddNATPF(n int, name string, rule PFRule) error { - return vbm("controlvm", m.Name, fmt.Sprintf("natpf%d", n), +func (m *Machine) AddNATPF(n int, name string, rule driver.PFRule) error { + return vbm("modifyvm", m.Name, fmt.Sprintf("--natpf%d", n), fmt.Sprintf("%s,%s", name, rule.Format())) } @@ -388,7 +698,7 @@ func (m *Machine) DelNATPF(n int, name string) error { } // SetNIC set the n-th NIC. -func (m *Machine) SetNIC(n int, nic NIC) error { +func (m *Machine) SetNIC(n int, nic driver.NIC) error { args := []string{"modifyvm", m.Name, fmt.Sprintf("--nic%d", n), string(nic.Network), fmt.Sprintf("--nictype%d", n), string(nic.Hardware), @@ -402,7 +712,7 @@ func (m *Machine) SetNIC(n int, nic NIC) error { } // AddStorageCtl adds a storage controller with the given name. -func (m *Machine) AddStorageCtl(name string, ctl StorageController) error { +func (m *Machine) AddStorageCtl(name string, ctl driver.StorageController) error { args := []string{"storagectl", m.Name, "--name", name} if ctl.SysBus != "" { args = append(args, "--add", string(ctl.SysBus)) @@ -424,7 +734,7 @@ func (m *Machine) DelStorageCtl(name string) error { } // AttachStorage attaches a storage medium to the named storage controller. -func (m *Machine) AttachStorage(ctlName string, medium StorageMedium) error { +func (m *Machine) AttachStorage(ctlName string, medium driver.StorageMedium) error { return vbm("storageattach", m.Name, "--storagectl", ctlName, "--port", fmt.Sprintf("%d", medium.Port), "--device", fmt.Sprintf("%d", medium.Device), diff --git a/virtualbox/vbm.go b/virtualbox/vbm.go index dd0df6f..ed166dd 100644 --- a/virtualbox/vbm.go +++ b/virtualbox/vbm.go @@ -3,6 +3,7 @@ package virtualbox import ( "bytes" "errors" + "io" "log" "os" "os/exec" @@ -10,17 +11,15 @@ import ( "regexp" "runtime" "strings" -) -var ( - VBM string // Path to VBoxManage utility. - Verbose bool // Verbose mode. + "github.com/boot2docker/boot2docker-cli/driver" ) func init() { - VBM = "VBoxManage" - if p := os.Getenv("VBOX_INSTALL_PATH"); p != "" && runtime.GOOS == "windows" { - VBM = filepath.Join(p, "VBoxManage.exe") + if runtime.GOOS == "darwin" { + // remove DYLD_LIBRARY_PATH and LD_LIBRARY_PATH as they break VBoxManage on OSX + os.Unsetenv("DYLD_LIBRARY_PATH") + os.Unsetenv("LD_LIBRARY_PATH") } } @@ -32,17 +31,15 @@ var ( ) var ( - ErrMachineExist = errors.New("machine already exists") - ErrMachineNotExist = errors.New("machine does not exist") - ErrVBMNotFound = errors.New("VBoxManage not found") + ErrVBMNotFound = errors.New("VBoxManage not found") ) func vbm(args ...string) error { - cmd := exec.Command(VBM, args...) - if Verbose { + cmd := exec.Command(cfg.VBM, args...) + if verbose { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - log.Printf("executing: %v %v", VBM, strings.Join(args, " ")) + log.Printf("executing: %v %v", cfg.VBM, strings.Join(args, " ")) } if err := cmd.Run(); err != nil { if ee, ok := err.(*exec.Error); ok && ee == exec.ErrNotFound { @@ -54,10 +51,10 @@ func vbm(args ...string) error { } func vbmOut(args ...string) (string, error) { - cmd := exec.Command(VBM, args...) - if Verbose { + cmd := exec.Command(cfg.VBM, args...) + if verbose { cmd.Stderr = os.Stderr - log.Printf("executing: %v %v", VBM, strings.Join(args, " ")) + log.Printf("executing: %v %v", cfg.VBM, strings.Join(args, " ")) } b, err := cmd.Output() @@ -70,9 +67,9 @@ func vbmOut(args ...string) (string, error) { } func vbmOutErr(args ...string) (string, string, error) { - cmd := exec.Command(VBM, args...) - if Verbose { - log.Printf("executing: %v %v", VBM, strings.Join(args, " ")) + cmd := exec.Command(cfg.VBM, args...) + if verbose { + log.Printf("executing: %v %v", cfg.VBM, strings.Join(args, " ")) } var stdout bytes.Buffer var stderr bytes.Buffer @@ -86,3 +83,89 @@ func vbmOutErr(args ...string) (string, string, error) { } return stdout.String(), stderr.String(), err } + +// Get or create the hostonly network interface +func getHostOnlyNetworkInterface(mc *driver.MachineConfig) (string, error) { + // Check if the interface/dhcp exists. + nets, err := HostonlyNets() + if err != nil { + return "", err + } + + dhcps, err := DHCPs() + if err != nil { + return "", err + } + + for _, n := range nets { + if dhcp, ok := dhcps[n.NetworkName]; ok { + if dhcp.IPv4.IP.Equal(mc.DHCPIP) && + dhcp.IPv4.Mask.String() == mc.NetMask.String() && + dhcp.LowerIP.Equal(mc.LowerIP) && + dhcp.UpperIP.Equal(mc.UpperIP) && + dhcp.Enabled == mc.DHCPEnabled { + return n.Name, nil + } + } + } + + // No existing host-only interface found. Create a new one. + hostonlyNet, err := CreateHostonlyNet() + if err != nil { + return "", err + } + hostonlyNet.IPv4.IP = mc.HostIP + hostonlyNet.IPv4.Mask = mc.NetMask + if err := hostonlyNet.Config(); err != nil { + return "", err + } + + // Create and add a DHCP server to the host-only network + dhcp := driver.DHCP{} + dhcp.IPv4.IP = mc.DHCPIP + dhcp.IPv4.Mask = mc.NetMask + dhcp.LowerIP = mc.LowerIP + dhcp.UpperIP = mc.UpperIP + dhcp.Enabled = true + if err := AddHostonlyDHCP(hostonlyNet.Name, dhcp); err != nil { + return "", err + } + return hostonlyNet.Name, nil +} + +// Copy disk image from given source path to destination +func copyDiskImage(dst, src string) (err error) { + // Open source disk image + srcImg, err := os.Open(src) + if err != nil { + return err + } + defer func() { + if ee := srcImg.Close(); ee != nil { + err = ee + } + }() + dstImg, err := os.Create(dst) + if err != nil { + return err + } + defer func() { + if ee := dstImg.Close(); ee != nil { + err = ee + } + }() + _, err = io.Copy(dstImg, srcImg) + return err +} + +// Make a boot2docker VM disk image with the given size (in MB). +func makeDiskImage(dest string, size uint, initialBytes []byte) error { + // Create the dest dir. + if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil { + return err + } + // Fill in the magic string so boot2docker VM will detect this and format + // the disk upon first boot. + raw := bytes.NewReader(initialBytes) + return MakeDiskImage(dest, size, raw) +} diff --git a/virtualbox/vbm_test.go b/virtualbox/vbm_test.go index 0355a11..2ebe150 100644 --- a/virtualbox/vbm_test.go +++ b/virtualbox/vbm_test.go @@ -5,7 +5,7 @@ import ( ) func init() { - Verbose = true + verbose = true } func TestVBMOut(t *testing.T) {