From 9b5f395c60ea475fa9d19f9509fd8a1bb95bc459 Mon Sep 17 00:00:00 2001 From: Nathan LeClaire Date: Mon, 30 Mar 2015 15:22:32 -0700 Subject: [PATCH] Implement upgrade functionality for boot2docker Signed-off-by: Nathan LeClaire --- docs/index.md | 34 +++++++++++- drivers/drivers.go | 14 +++++ drivers/virtualbox/virtualbox.go | 46 +++-------------- libmachine/host.go | 25 +++------ libmachine/provision/boot2docker.go | 50 ++++++++++++++++++ test/integration/driver-virtualbox.bats | 15 ++++++ utils/b2d.go | 69 +++++++++++++++++++++++++ utils/utils.go | 2 +- 8 files changed, 193 insertions(+), 62 deletions(-) diff --git a/docs/index.md b/docs/index.md index aef1dd1f6c..865dd3676d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -579,12 +579,26 @@ dev * virtualbox Stopped #### upgrade -Upgrade a machine to the latest version of Docker. +Upgrade a machine to the latest version of Docker. If the machine uses Ubuntu +as the underlying operating system, it will upgrade the package `lxc-docker` +(our recommended install method). If the machine uses boot2docker, this command +will download the latest boot2docker ISO and replace the machine's existing ISO +with the latest. ``` $ docker-machine upgrade dev +INFO[0000] Stopping machine to do the upgrade... +INFO[0005] Upgrading machine dev... +INFO[0006] Downloading latest boot2docker release to /tmp/store/cache/boot2docker.iso... +INFO[0008] Starting machine back up... +INFO[0008] Waiting for VM to start... ``` +> **Note**: If you are using a custom boot2docker ISO specified using +> `--virtualbox-boot2docker-url` or an equivalent flag, running an upgrade on +> that machine will completely replace the specified ISO with the latest +> "vanilla" boot2docker ISO available. + #### url Get the URL of a host @@ -826,7 +840,22 @@ Options: - `--virtualbox-memory`: Size of memory for the host in MB. Default: `1024` - `--virtualbox-cpu-count`: Number of CPUs to use to create the VM. Defaults to number of available CPUs. -The VirtualBox driver uses the latest boot2docker image. +The `--virtualbox-boot2docker-url` flag takes a few different forms. By +default, if no value is specified for this flag, Machine will check locally for +a boot2docker ISO. If one is found, that will be used as the ISO for the +created machine. If one is not found, the latest ISO release available on +[boot2docker/boot2docker](https://github.com/boot2docker/boot2docker) will be +downloaded and stored locally for future use. Note that this means you must run +`docker-machine upgrade` deliberately on a machine if you wish to update the "cached" +boot2docker ISO. + +This is the default behavior (when `--virtualbox-boot2docker-url=""`), but the +option also supports specifying ISOs by the `http://` and `file://` protocols. +`file://` will look at the path specified locally to locate the ISO: for +instance, you could specify `--virtualbox-boot2docker-url +file://$HOME/Downloads/rc.iso` to test out a release candidate ISO that you have +downloaded already. You could also just get an ISO straight from the Internet +using the `http://` form. Environment variables: @@ -840,6 +869,7 @@ variable and CLI option are provided the CLI option takes the precedence. | `VIRTUALBOX_DISK_SIZE` | `--virtualbox-disk-size` | | `VIRTUALBOX_BOOT2DOCKER_URL` | `--virtualbox-boot2docker-url` | + #### VMware Fusion Creates machines locally on [VMware Fusion](http://www.vmware.com/products/fusion). Requires VMware Fusion to be installed. diff --git a/drivers/drivers.go b/drivers/drivers.go index 46ad7eacdb..83b4fc196d 100644 --- a/drivers/drivers.go +++ b/drivers/drivers.go @@ -6,6 +6,7 @@ import ( "os/exec" "sort" + log "github.com/Sirupsen/logrus" "github.com/codegangsta/cli" "github.com/docker/machine/provider" "github.com/docker/machine/ssh" @@ -175,3 +176,16 @@ func GetSSHCommandFromDriver(d Driver, args ...string) (*exec.Cmd, error) { return ssh.GetSSHCommand(host, port, user, keyPath, args...), nil } + +func MachineInState(d Driver, desiredState state.State) func() bool { + return func() bool { + currentState, err := d.GetState() + if err != nil { + log.Debugf("Error getting machine state: %s", err) + } + if currentState == desiredState { + return true + } + return false + } +} diff --git a/drivers/virtualbox/virtualbox.go b/drivers/virtualbox/virtualbox.go index 1720dd5db6..f7a31b45a1 100644 --- a/drivers/virtualbox/virtualbox.go +++ b/drivers/virtualbox/virtualbox.go @@ -158,8 +158,7 @@ func (d *Driver) PreCreateCheck() error { func (d *Driver) Create() error { var ( - err error - isoURL string + err error ) // Check that VBoxManage exists and works @@ -172,46 +171,13 @@ func (d *Driver) Create() error { return err } - b2dutils := utils.NewB2dUtils("", "") - imgPath := utils.GetMachineCacheDir() - isoFilename := "boot2docker.iso" - commonIsoPath := filepath.Join(imgPath, "boot2docker.iso") - // just in case boot2docker.iso has been manually deleted - if _, err := os.Stat(imgPath); os.IsNotExist(err) { - if err := os.Mkdir(imgPath, 0700); err != nil { - return err - } - } - - if d.Boot2DockerURL != "" { - isoURL = d.Boot2DockerURL - log.Infof("Downloading %s from %s...", isoFilename, isoURL) - if err := b2dutils.DownloadISO(d.storePath, isoFilename, isoURL); err != nil { - return err - } - } else { - // todo: check latest release URL, download if it's new - // until then always use "latest" - isoURL, err = b2dutils.GetLatestBoot2DockerReleaseURL() - if err != nil { - log.Warnf("Unable to check for the latest release: %s", err) - } - - if _, err := os.Stat(commonIsoPath); os.IsNotExist(err) { - log.Infof("Downloading %s to %s...", isoFilename, commonIsoPath) - if err := b2dutils.DownloadISO(imgPath, isoFilename, isoURL); err != nil { - return err - } - } - - isoDest := filepath.Join(d.storePath, isoFilename) - if err := utils.CopyFile(commonIsoPath, isoDest); err != nil { - return err - } - } - log.Infof("Creating SSH key...") + b2dutils := utils.NewB2dUtils("", "") + if err := b2dutils.CopyIsoToMachineDir(d.Boot2DockerURL, d.MachineName); err != nil { + return err + } + if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil { return err } diff --git a/libmachine/host.go b/libmachine/host.go index 1b7d9e1caa..b1c5d2dd02 100644 --- a/libmachine/host.go +++ b/libmachine/host.go @@ -146,19 +146,6 @@ func (h *Host) GetSSHCommand(args ...string) (*exec.Cmd, error) { return cmd, nil } -func (h *Host) MachineInState(desiredState state.State) func() bool { - return func() bool { - currentState, err := h.Driver.GetState() - if err != nil { - log.Debugf("Error getting machine state: %s", err) - } - if currentState == desiredState { - return true - } - return false - } -} - func (h *Host) Start() error { if err := h.Driver.Start(); err != nil { return err @@ -168,7 +155,7 @@ func (h *Host) Start() error { return err } - return utils.WaitFor(h.MachineInState(state.Running)) + return utils.WaitFor(drivers.MachineInState(h.Driver, state.Running)) } func (h *Host) Stop() error { @@ -180,7 +167,7 @@ func (h *Host) Stop() error { return err } - return utils.WaitFor(h.MachineInState(state.Stopped)) + return utils.WaitFor(drivers.MachineInState(h.Driver, state.Stopped)) } func (h *Host) Kill() error { @@ -192,16 +179,16 @@ func (h *Host) Kill() error { return err } - return utils.WaitFor(h.MachineInState(state.Stopped)) + return utils.WaitFor(drivers.MachineInState(h.Driver, state.Stopped)) } func (h *Host) Restart() error { - if h.MachineInState(state.Running)() { + if drivers.MachineInState(h.Driver, state.Running)() { if err := h.Stop(); err != nil { return err } - if err := utils.WaitFor(h.MachineInState(state.Stopped)); err != nil { + if err := utils.WaitFor(drivers.MachineInState(h.Driver, state.Stopped)); err != nil { return err } } @@ -210,7 +197,7 @@ func (h *Host) Restart() error { return err } - if err := utils.WaitFor(h.MachineInState(state.Running)); err != nil { + if err := utils.WaitFor(drivers.MachineInState(h.Driver, state.Running)); err != nil { return err } diff --git a/libmachine/provision/boot2docker.go b/libmachine/provision/boot2docker.go index 5c9a3ef992..5a2b7acb5b 100644 --- a/libmachine/provision/boot2docker.go +++ b/libmachine/provision/boot2docker.go @@ -2,14 +2,17 @@ package provision import ( "bytes" + "errors" "fmt" "os/exec" "path" + log "github.com/Sirupsen/logrus" "github.com/docker/machine/drivers" "github.com/docker/machine/libmachine/auth" "github.com/docker/machine/libmachine/provision/pkgaction" "github.com/docker/machine/libmachine/swarm" + "github.com/docker/machine/state" "github.com/docker/machine/utils" ) @@ -46,7 +49,54 @@ func (provisioner *Boot2DockerProvisioner) Service(name string, action pkgaction return nil } +func (provisioner *Boot2DockerProvisioner) upgradeIso() error { + log.Infof("Stopping machine to do the upgrade...") + + switch provisioner.Driver.DriverName() { + case "vmwarefusion", "vmwarevsphere": + return errors.New("Upgrade functionality is currently not supported for these providers, as they use a custom ISO.") + } + + if err := provisioner.Driver.Stop(); err != nil { + return err + } + + if err := utils.WaitFor(drivers.MachineInState(provisioner.Driver, state.Stopped)); err != nil { + return err + } + + machineName := provisioner.GetDriver().GetMachineName() + + log.Infof("Upgrading machine %s...", machineName) + + b2dutils := utils.NewB2dUtils("", "") + + // Usually we call this implicitly, but call it here explicitly to get + // the latest boot2docker ISO. + if err := b2dutils.DownloadLatestBoot2Docker(); err != nil { + return err + } + + // Copy the latest version of boot2docker ISO to the machine's directory + if err := b2dutils.CopyIsoToMachineDir("", machineName); err != nil { + return err + } + + log.Infof("Starting machine back up...") + + if err := provisioner.Driver.Start(); err != nil { + return err + } + + return utils.WaitFor(drivers.MachineInState(provisioner.Driver, state.Running)) +} + func (provisioner *Boot2DockerProvisioner) Package(name string, action pkgaction.PackageAction) error { + if name == "docker" && action == pkgaction.Upgrade { + if err := provisioner.upgradeIso(); err != nil { + return err + } + } return nil } diff --git a/test/integration/driver-virtualbox.bats b/test/integration/driver-virtualbox.bats index 9427c00710..8a290c95b1 100644 --- a/test/integration/driver-virtualbox.bats +++ b/test/integration/driver-virtualbox.bats @@ -36,6 +36,12 @@ findCPUCount() { run bash -c "VBoxManage showvminfo --machinereadable $NAME | grep cpus= | cut -d'=' -f2" } +buildMachineWithOldIsoCheckUpgrade() { + run wget https://github.com/boot2docker/boot2docker/releases/download/v1.4.1/boot2docker.iso -O $MACHINE_STORAGE_PATH/cache/boot2docker.iso + run machine create -d virtualbox $NAME + run machine upgrade $NAME +} + @test "$DRIVER: machine should not exist" { run machine active $NAME [ "$status" -eq 1 ] @@ -316,6 +322,15 @@ findCPUCount() { [ "$status" -eq 0 ] } +@test "$DRIVER: upgrade should work" { + buildMachineWithOldIsoCheckUpgrade + [ "$status" -eq 0 ] +} + +@test "$DRIVER: remove machine after upgrade test" { + run machine rm -f $NAME +} + # Cleanup of machine store should always be the last 'test' @test "$DRIVER: cleanup" { run rm -rf $MACHINE_STORAGE_PATH diff --git a/utils/b2d.go b/utils/b2d.go index 0d57811a81..38df28870c 100644 --- a/utils/b2d.go +++ b/utils/b2d.go @@ -11,6 +11,8 @@ import ( "os" "path/filepath" "time" + + log "github.com/Sirupsen/logrus" ) const ( @@ -36,6 +38,9 @@ func getClient() *http.Client { } type B2dUtils struct { + isoFilename string + commonIsoPath string + imgCachePath string githubApiBaseUrl string githubBaseUrl string } @@ -43,6 +48,8 @@ type B2dUtils struct { func NewB2dUtils(githubApiBaseUrl, githubBaseUrl string) *B2dUtils { defaultBaseApiUrl := "https://api.github.com" defaultBaseUrl := "https://github.com" + imgCachePath := GetMachineCacheDir() + isoFilename := "boot2docker.iso" if githubApiBaseUrl == "" { githubApiBaseUrl = defaultBaseApiUrl @@ -53,6 +60,9 @@ func NewB2dUtils(githubApiBaseUrl, githubBaseUrl string) *B2dUtils { } return &B2dUtils{ + isoFilename: isoFilename, + imgCachePath: GetMachineCacheDir(), + commonIsoPath: filepath.Join(imgCachePath, isoFilename), githubApiBaseUrl: githubApiBaseUrl, githubBaseUrl: githubBaseUrl, } @@ -128,3 +138,62 @@ func (b *B2dUtils) DownloadISO(dir, file, isoUrl string) error { return nil } + +func (b *B2dUtils) DownloadLatestBoot2Docker() error { + latestReleaseUrl, err := b.GetLatestBoot2DockerReleaseURL() + if err != nil { + return err + } + + log.Infof("Downloading latest boot2docker release to %s...", b.commonIsoPath) + if err := b.DownloadISO(b.imgCachePath, b.isoFilename, latestReleaseUrl); err != nil { + return err + } + + return nil +} + +func (b *B2dUtils) CopyIsoToMachineDir(isoURL, machineName string) error { + machinesDir := GetMachineDir() + machineIsoPath := filepath.Join(machinesDir, machineName, b.isoFilename) + + // just in case the cache dir has been manually deleted, + // check for it and recreate it if it's gone + if _, err := os.Stat(b.imgCachePath); os.IsNotExist(err) { + log.Infof("Image cache does not exist, creating it at %s...", b.imgCachePath) + if err := os.Mkdir(b.imgCachePath, 0700); err != nil { + return err + } + } + + // By default just copy the existing "cached" iso to + // the machine's directory... + if isoURL == "" { + if err := b.copyDefaultIsoToMachine(machineIsoPath); err != nil { + return err + } + } else { + // But if ISO is specified go get it directly + log.Infof("Downloading %s from %s...", b.isoFilename, isoURL) + if err := b.DownloadISO(filepath.Join(machinesDir, machineName), b.isoFilename, isoURL); err != nil { + return err + } + } + + return nil +} + +func (b *B2dUtils) copyDefaultIsoToMachine(machineIsoPath string) error { + if _, err := os.Stat(b.commonIsoPath); os.IsNotExist(err) { + log.Info("No default boot2docker iso found locally, downloading the latest release...") + if err := b.DownloadLatestBoot2Docker(); err != nil { + return err + } + } + + if err := CopyFile(b.commonIsoPath, machineIsoPath); err != nil { + return err + } + + return nil +} diff --git a/utils/utils.go b/utils/utils.go index a6c4e4fd01..97a56e7743 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -100,7 +100,7 @@ func WaitForDocker(ip string, daemonPort int) error { return WaitFor(func() bool { conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", ip, daemonPort)) if err != nil { - log.Debug("Got an error it was", err) + log.Debugf("Got an error it was %s", err) return false } conn.Close()