diff --git a/drivers/virtualbox/disk.go b/drivers/virtualbox/disk.go index 83af1c9d46..82f064f6f3 100644 --- a/drivers/virtualbox/disk.go +++ b/drivers/virtualbox/disk.go @@ -1,10 +1,120 @@ package virtualbox +import ( + "fmt" + "io" + "os" + "os/exec" + + "github.com/docker/machine/libmachine/log" + "github.com/docker/machine/libmachine/mcnutils" +) + type VirtualDisk struct { UUID string Path string } +type DiskCreator interface { + Create(size int, publicSSHKeyPath, diskPath string) error +} + +func NewDiskCreator() DiskCreator { + return &defaultDiskCreator{} +} + +type defaultDiskCreator struct{} + +// Make a boot2docker VM disk image. +func (c *defaultDiskCreator) Create(size int, publicSSHKeyPath, diskPath string) error { + log.Debugf("Creating %d MB hard disk image...", size) + + tarBuf, err := mcnutils.MakeDiskImage(publicSSHKeyPath) + if err != nil { + return err + } + + log.Debug("Calling inner createDiskImage") + + return createDiskImage(diskPath, size, tarBuf) +} + +// createDiskImage makes a disk image at dest with the given size in MB. If r is +// not nil, it will be read as a raw disk image to convert from. +func createDiskImage(dest string, size int, r io.Reader) error { + // Convert a raw image from stdin to the dest VMDK image. + sizeBytes := int64(size) << 20 // usually won't fit in 32-bit int (max 2GB) + // FIXME: why isn't this just using the vbm*() functions? + cmd := exec.Command(vboxManageCmd, "convertfromraw", "stdin", dest, + fmt.Sprintf("%d", sizeBytes), "--format", "VMDK") + + log.Debug(cmd) + + if os.Getenv("MACHINE_DEBUG") != "" { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + + stdin, err := cmd.StdinPipe() + if err != nil { + return err + } + + log.Debug("Starting command") + + if err := cmd.Start(); err != nil { + return err + } + + log.Debug("Copying to stdin") + + n, err := io.Copy(stdin, r) + if err != nil { + return err + } + + log.Debug("Filling zeroes") + + // The total number of bytes written to stdin must match sizeBytes, or + // VBoxManage.exe on Windows will fail. Fill remaining with zeros. + if left := sizeBytes - n; left > 0 { + if err := zeroFill(stdin, left); err != nil { + return err + } + } + + log.Debug("Closing STDIN") + + // cmd won't exit until the stdin is closed. + if err := stdin.Close(); err != nil { + return err + } + + log.Debug("Waiting on cmd") + + return cmd.Wait() +} + +// zeroFill writes n zero bytes into w. +func zeroFill(w io.Writer, n int64) error { + const blocksize = 32 << 10 + zeros := make([]byte, blocksize) + var k int + var err error + for n > 0 { + if n > blocksize { + k, err = w.Write(zeros) + } else { + k, err = w.Write(zeros[:n]) + } + if err != nil { + return err + } + n -= int64(k) + } + return nil +} + func getVMDiskInfo(name string, vbox VBoxManager) (*VirtualDisk, error) { out, err := vbox.vbmOut("showvminfo", name, "--machinereadable") if err != nil { diff --git a/drivers/virtualbox/ip.go b/drivers/virtualbox/ip.go new file mode 100644 index 0000000000..be22a4fb7a --- /dev/null +++ b/drivers/virtualbox/ip.go @@ -0,0 +1,36 @@ +package virtualbox + +import ( + "time" + + "github.com/docker/machine/libmachine/drivers" + "github.com/docker/machine/libmachine/mcnutils" +) + +// IPWaiter waits for an IP to be configured. +type IPWaiter interface { + Wait(d *Driver) error +} + +func NewIPWaiter() IPWaiter { + return &sshIPWaiter{} +} + +type sshIPWaiter struct{} + +func (w *sshIPWaiter) Wait(d *Driver) error { + // Wait for SSH over NAT to be available before returning to user + if err := drivers.WaitForSSH(d); err != nil { + return err + } + + // Bail if we don't get an IP from DHCP after a given number of seconds. + if err := mcnutils.WaitForSpecific(d.hostOnlyIPAvailable, 5, 4*time.Second); err != nil { + return err + } + + var err error + d.IPAddress, err = d.GetIP() + + return err +} diff --git a/drivers/virtualbox/misc.go b/drivers/virtualbox/misc.go new file mode 100644 index 0000000000..dafec76799 --- /dev/null +++ b/drivers/virtualbox/misc.go @@ -0,0 +1,106 @@ +package virtualbox + +import ( + "bufio" + "math/rand" + "os" + + "time" + + "github.com/docker/machine/libmachine/mcnutils" + "github.com/docker/machine/libmachine/ssh" +) + +// B2DUpdater describes the interactions with bd2. +type B2DUpdater interface { + UpdateISOCache(storePath, isoURL string) error + CopyIsoToMachineDir(storePath, machineName, isoURL string) error +} + +func NewB2DUpdater() B2DUpdater { + return &b2dUtilsUpdater{} +} + +type b2dUtilsUpdater struct{} + +func (u *b2dUtilsUpdater) CopyIsoToMachineDir(storePath, machineName, isoURL string) error { + return mcnutils.NewB2dUtils(storePath).CopyIsoToMachineDir(isoURL, machineName) +} + +func (u *b2dUtilsUpdater) UpdateISOCache(storePath, isoURL string) error { + return mcnutils.NewB2dUtils(storePath).UpdateISOCache(isoURL) +} + +// SSHKeyGenerator describes the generation of ssh keys. +type SSHKeyGenerator interface { + Generate(path string) error +} + +func NewSSHKeyGenerator() SSHKeyGenerator { + return &defaultSSHKeyGenerator{} +} + +type defaultSSHKeyGenerator struct{} + +func (g *defaultSSHKeyGenerator) Generate(path string) error { + return ssh.GenerateSSHKey(path) +} + +// LogsReader describes the reading of VBox.log +type LogsReader interface { + Read(path string) ([]string, error) +} + +func NewLogsReader() LogsReader { + return &vBoxLogsReader{} +} + +type vBoxLogsReader struct{} + +func (c *vBoxLogsReader) Read(path string) ([]string, error) { + file, err := os.Open(path) + if err != nil { + return []string{}, err + } + + defer file.Close() + + lines := []string{} + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + return lines, nil +} + +// RandomInter returns random int values. +type RandomInter interface { + RandomInt(n int) int +} + +func NewRandomInter() RandomInter { + return &defaultRandomInter{} +} + +type defaultRandomInter struct{} + +func (d *defaultRandomInter) RandomInt(n int) int { + return rand.Intn(n) +} + +// Sleeper sleeps for given duration. +type Sleeper interface { + Sleep(d time.Duration) +} + +func NewSleeper() Sleeper { + return &defaultSleeper{} +} + +type defaultSleeper struct{} + +func (s *defaultSleeper) Sleep(d time.Duration) { + time.Sleep(d) +} diff --git a/drivers/virtualbox/virtualbox.go b/drivers/virtualbox/virtualbox.go index 498696c717..ad12955d6a 100644 --- a/drivers/virtualbox/virtualbox.go +++ b/drivers/virtualbox/virtualbox.go @@ -1,11 +1,8 @@ package virtualbox import ( - "bufio" "errors" "fmt" - "io" - "math/rand" "net" "os" "os/exec" @@ -20,7 +17,6 @@ import ( "github.com/docker/machine/libmachine/log" "github.com/docker/machine/libmachine/mcnflag" "github.com/docker/machine/libmachine/mcnutils" - "github.com/docker/machine/libmachine/ssh" "github.com/docker/machine/libmachine/state" ) @@ -43,8 +39,15 @@ var ( ) type Driver struct { - VBoxManager *drivers.BaseDriver + VBoxManager + b2dUpdater B2DUpdater + sshKeyGenerator SSHKeyGenerator + diskCreator DiskCreator + logsReader LogsReader + ipWaiter IPWaiter + randomInter RandomInter + sleeper Sleeper CPU int Memory int DiskSize int @@ -62,17 +65,24 @@ type Driver struct { // NewDriver creates a new VirtualBox driver with default settings. func NewDriver(hostName, storePath string) *Driver { return &Driver{ - VBoxManager: NewVBoxManager(), - BaseDriver: &drivers.BaseDriver{ - MachineName: hostName, - StorePath: storePath, - }, + VBoxManager: NewVBoxManager(), + b2dUpdater: NewB2DUpdater(), + sshKeyGenerator: NewSSHKeyGenerator(), + diskCreator: NewDiskCreator(), + logsReader: NewLogsReader(), + ipWaiter: NewIPWaiter(), + randomInter: NewRandomInter(), + sleeper: NewSleeper(), Memory: defaultMemory, CPU: defaultCPU, DiskSize: defaultDiskSize, HostOnlyCIDR: defaultHostOnlyCIDR, HostOnlyNicType: defaultHostOnlyNictype, HostOnlyPromiscMode: defaultHostOnlyPromiscMode, + BaseDriver: &drivers.BaseDriver{ + MachineName: hostName, + StorePath: storePath, + }, } } @@ -223,8 +233,7 @@ func (d *Driver) PreCreateCheck() error { // Downloading boot2docker to cache should be done here to make sure // that a download failure will not leave a machine half created. - b2dutils := mcnutils.NewB2dUtils(d.StorePath) - if err := b2dutils.UpdateISOCache(d.Boot2DockerURL); err != nil { + if err := d.b2dUpdater.UpdateISOCache(d.StorePath, d.Boot2DockerURL); err != nil { return err } @@ -241,22 +250,19 @@ func (d *Driver) IsVTXDisabledInTheVM() (bool, error) { logPath := filepath.Join(d.ResolveStorePath(d.MachineName), "Logs", "VBox.log") log.Debugf("Checking vm logs: %s", logPath) - file, err := os.Open(logPath) + lines, err := d.logsReader.Read(logPath) if err != nil { return true, err } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - if strings.Contains(scanner.Text(), "VT-x is disabled") { + for _, line := range lines { + if strings.Contains(line, "VT-x is disabled") { return true, nil } - if strings.Contains(scanner.Text(), "the host CPU does NOT support HW virtualization") { + if strings.Contains(line, "the host CPU does NOT support HW virtualization") { return true, nil } - if strings.Contains(scanner.Text(), "VERR_VMX_UNABLE_TO_START_VM") { + if strings.Contains(line, "VERR_VMX_UNABLE_TO_START_VM") { return true, nil } } @@ -265,8 +271,16 @@ func (d *Driver) IsVTXDisabledInTheVM() (bool, error) { } func (d *Driver) Create() error { - b2dutils := mcnutils.NewB2dUtils(d.StorePath) - if err := b2dutils.CopyIsoToMachineDir(d.Boot2DockerURL, d.MachineName); err != nil { + if err := d.CreateVM(); err != nil { + return err + } + + log.Info("Starting the VM...") + return d.Start() +} + +func (d *Driver) CreateVM() error { + if err := d.b2dUpdater.CopyIsoToMachineDir(d.StorePath, d.MachineName, d.Boot2DockerURL); err != nil { return err } @@ -308,12 +322,12 @@ func (d *Driver) Create() error { } } else { log.Infof("Creating SSH key...") - if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil { + if err := d.sshKeyGenerator.Generate(d.GetSSHKeyPath()); err != nil { return err } log.Debugf("Creating disk image...") - if err := d.generateDiskImage(d.DiskSize); err != nil { + if err := d.diskCreator.Create(d.DiskSize, d.publicSSHKeyPath(), d.diskPath()); err != nil { return err } } @@ -438,8 +452,7 @@ func (d *Driver) Create() error { } } - log.Info("Starting the VM...") - return d.Start() + return nil } func (d *Driver) hostOnlyIPAvailable() bool { @@ -502,7 +515,7 @@ func (d *Driver) Start() error { } log.Infof("Waiting for an IP...") - if err := d.waitForIP(); err != nil { + if err := d.ipWaiter.Wait(d); err != nil { return err } @@ -535,7 +548,7 @@ func (d *Driver) Start() error { } // We have to be sure the host-only adapter is not used by the VM - time.Sleep(5 * time.Second) + d.sleeper.Sleep(5 * time.Second) log.Debugf("Fixing %+v...", hostOnlyAdapter) if err := hostOnlyAdapter.SaveIPv4(d.VBoxManager); err != nil { @@ -543,14 +556,14 @@ func (d *Driver) Start() error { } // We have to be sure the adapter is updated before starting the VM - time.Sleep(5 * time.Second) + d.sleeper.Sleep(5 * time.Second) if err := d.vbm("startvm", d.MachineName, "--type", "headless"); err != nil { return fmt.Errorf("Unable to start the VM: %s", err) } log.Infof("Waiting for an IP...") - return d.waitForIP() + return d.ipWaiter.Wait(d) } func (d *Driver) Stop() error { @@ -575,7 +588,7 @@ func (d *Driver) Stop() error { return err } if s == state.Running { - time.Sleep(1 * time.Second) + d.sleeper.Sleep(1 * time.Second) } else { break } @@ -594,30 +607,13 @@ func (d *Driver) Restart() error { d.IPAddress = "" - return d.waitForIP() + return d.ipWaiter.Wait(d) } func (d *Driver) Kill() error { return d.vbm("controlvm", d.MachineName, "poweroff") } -func (d *Driver) waitForIP() error { - // Wait for SSH over NAT to be available before returning to user - if err := drivers.WaitForSSH(d); err != nil { - return err - } - - // Bail if we don't get an IP from DHCP after a given number of seconds. - if err := mcnutils.WaitForSpecific(d.hostOnlyIPAvailable, 5, 4*time.Second); err != nil { - return err - } - - var err error - d.IPAddress, err = d.GetIP() - - return err -} - func (d *Driver) Remove() error { s, err := d.GetState() if err != nil { @@ -637,7 +633,7 @@ func (d *Driver) Remove() error { } } // vbox will not release it's lock immediately after the stop - time.Sleep(1 * time.Second) + d.sleeper.Sleep(1 * time.Second) return d.vbm("unregistervm", "--delete", d.MachineName) } @@ -705,20 +701,6 @@ func (d *Driver) diskPath() string { return d.ResolveStorePath("disk.vmdk") } -// Make a boot2docker VM disk image. -func (d *Driver) generateDiskImage(size int) error { - log.Debugf("Creating %d MB hard disk image...", size) - - tarBuf, err := mcnutils.MakeDiskImage(d.publicSSHKeyPath()) - if err != nil { - return err - } - - log.Debug("Calling inner createDiskImage") - - return createDiskImage(d.diskPath(), size, tarBuf) -} - func (d *Driver) setupHostOnlyNetwork(machineName string) (*hostOnlyNetwork, error) { hostOnlyCIDR := d.HostOnlyCIDR @@ -744,7 +726,7 @@ func (d *Driver) setupHostOnlyNetwork(machineName string) (*hostOnlyNetwork, err return nil, err } - dhcpAddr, err := getRandomIPinSubnet(ip) + dhcpAddr, err := getRandomIPinSubnet(d, ip) if err != nil { return nil, err } @@ -788,82 +770,6 @@ func parseAndValidateCIDR(hostOnlyCIDR string) (net.IP, *net.IPNet, error) { return ip, network, nil } -// createDiskImage makes a disk image at dest with the given size in MB. If r is -// not nil, it will be read as a raw disk image to convert from. -func createDiskImage(dest string, size int, r io.Reader) error { - // Convert a raw image from stdin to the dest VMDK image. - sizeBytes := int64(size) << 20 // usually won't fit in 32-bit int (max 2GB) - // FIXME: why isn't this just using the vbm*() functions? - cmd := exec.Command(vboxManageCmd, "convertfromraw", "stdin", dest, - fmt.Sprintf("%d", sizeBytes), "--format", "VMDK") - - log.Debug(cmd) - - if os.Getenv("MACHINE_DEBUG") != "" { - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - } - - stdin, err := cmd.StdinPipe() - if err != nil { - return err - } - - log.Debug("Starting command") - - if err := cmd.Start(); err != nil { - return err - } - - log.Debug("Copying to stdin") - - n, err := io.Copy(stdin, r) - if err != nil { - return err - } - - log.Debug("Filling zeroes") - - // The total number of bytes written to stdin must match sizeBytes, or - // VBoxManage.exe on Windows will fail. Fill remaining with zeros. - if left := sizeBytes - n; left > 0 { - if err := zeroFill(stdin, left); err != nil { - return err - } - } - - log.Debug("Closing STDIN") - - // cmd won't exit until the stdin is closed. - if err := stdin.Close(); err != nil { - return err - } - - log.Debug("Waiting on cmd") - - return cmd.Wait() -} - -// zeroFill writes n zero bytes into w. -func zeroFill(w io.Writer, n int64) error { - const blocksize = 32 << 10 - zeros := make([]byte, blocksize) - var k int - var err error - for n > 0 { - if n > blocksize { - k, err = w.Write(zeros) - } else { - k, err = w.Write(zeros[:n]) - } - if err != nil { - return err - } - n -= int64(k) - } - return nil -} - // Select an available port, trying the specified // port first, falling back on an OS selected port. func getAvailableTCPPort(port int) (int, error) { @@ -910,14 +816,14 @@ func setPortForwarding(d *Driver, interfaceNum int, mapName, protocol string, gu // getRandomIPinSubnet returns a pseudo-random net.IP in the same // subnet as the IP passed -func getRandomIPinSubnet(baseIP net.IP) (net.IP, error) { +func getRandomIPinSubnet(d *Driver, baseIP net.IP) (net.IP, error) { var dhcpAddr net.IP nAddr := baseIP.To4() // select pseudo-random DHCP addr; make sure not to clash with the host // only try 5 times and bail if no random received for i := 0; i < 5; i++ { - n := rand.Intn(25) + n := d.randomInter.RandomInt(25) if byte(n) != nAddr[3] { dhcpAddr = net.IPv4(nAddr[0], nAddr[1], nAddr[2], byte(n)) break diff --git a/drivers/virtualbox/virtualbox_test.go b/drivers/virtualbox/virtualbox_test.go index d9eee6d100..ff5999740c 100644 --- a/drivers/virtualbox/virtualbox_test.go +++ b/drivers/virtualbox/virtualbox_test.go @@ -6,30 +6,15 @@ import ( "strings" "testing" + "fmt" + + "time" + "github.com/docker/machine/libmachine/drivers" "github.com/docker/machine/libmachine/state" "github.com/stretchr/testify/assert" ) -func TestDriverName(t *testing.T) { - driverName := newTestDriver("default").DriverName() - - assert.Equal(t, "virtualbox", driverName) -} - -func TestSSHHostname(t *testing.T) { - hostname, err := newTestDriver("default").GetSSHHostname() - - assert.Equal(t, "127.0.0.1", hostname) - assert.NoError(t, err) -} - -func TestDefaultSSHUsername(t *testing.T) { - username := newTestDriver("default").GetSSHUsername() - - assert.Equal(t, "docker", username) -} - type VBoxManagerMock struct { args string stdOut string @@ -54,6 +39,29 @@ func (v *VBoxManagerMock) vbmOutErr(args ...string) (string, string, error) { return "", "", errors.New("Invalid args") } +func newTestDriver(name string) *Driver { + return NewDriver(name, "") +} + +func TestDriverName(t *testing.T) { + driverName := newTestDriver("default").DriverName() + + assert.Equal(t, "virtualbox", driverName) +} + +func TestSSHHostname(t *testing.T) { + hostname, err := newTestDriver("default").GetSSHHostname() + + assert.Equal(t, "127.0.0.1", hostname) + assert.NoError(t, err) +} + +func TestDefaultSSHUsername(t *testing.T) { + username := newTestDriver("default").GetSSHUsername() + + assert.Equal(t, "docker", username) +} + func TestState(t *testing.T) { var tests = []struct { stdOut string @@ -108,9 +116,11 @@ func TestStateErrors(t *testing.T) { } func TestGetRandomIPinSubnet(t *testing.T) { + driver := newTestDriver("default") + // test IP 1.2.3.4 testIP := net.IPv4(byte(1), byte(2), byte(3), byte(4)) - newIP, err := getRandomIPinSubnet(testIP) + newIP, err := getRandomIPinSubnet(driver, testIP) if err != nil { t.Fatal(err) } @@ -187,12 +197,8 @@ func TestInvalidNetworkIpCIDR(t *testing.T) { assert.Nil(t, network) } -func newTestDriver(name string) *Driver { - return NewDriver(name, "") -} - func TestSetConfigFromFlags(t *testing.T) { - driver := NewDriver("default", "path") + driver := newTestDriver("default") checkFlags := &drivers.CheckDriverOptions{ FlagsValues: map[string]interface{}{}, @@ -204,3 +210,271 @@ func TestSetConfigFromFlags(t *testing.T) { assert.NoError(t, err) assert.Empty(t, checkFlags.InvalidFlags) } + +type MockCreateOperations struct { + test *testing.T + expectedCalls []Call + call int +} + +type Call struct { + signature string + output string + err error +} + +func (v *MockCreateOperations) vbm(args ...string) error { + _, _, err := v.vbmOutErr(args...) + return err +} + +func (v *MockCreateOperations) vbmOut(args ...string) (string, error) { + stdout, _, err := v.vbmOutErr(args...) + return stdout, err +} + +func (v *MockCreateOperations) vbmOutErr(args ...string) (string, string, error) { + output, err := v.doCall("vbm " + strings.Join(args, " ")) + return output, "", err +} + +func (v *MockCreateOperations) UpdateISOCache(storePath, isoURL string) error { + _, err := v.doCall("UpdateISOCache " + storePath + " " + isoURL) + return err +} + +func (v *MockCreateOperations) CopyIsoToMachineDir(storePath, machineName, isoURL string) error { + _, err := v.doCall("CopyIsoToMachineDir " + storePath + " " + machineName + " " + isoURL) + return err +} + +func (v *MockCreateOperations) Generate(path string) error { + _, err := v.doCall("Generate " + path) + return err +} + +func (v *MockCreateOperations) Create(size int, publicSSHKeyPath, diskPath string) error { + _, err := v.doCall("Create " + fmt.Sprintf("%d %s %s", size, publicSSHKeyPath, diskPath)) + return err +} + +func (v *MockCreateOperations) Read(path string) ([]string, error) { + _, err := v.doCall("Read " + path) + return []string{}, err +} + +func (v *MockCreateOperations) Wait(d *Driver) error { + _, err := v.doCall("WaitIP") + return err +} + +func (v *MockCreateOperations) RandomInt(n int) int { + return 6 +} + +func (v *MockCreateOperations) Sleep(d time.Duration) { + v.doCall("Sleep " + fmt.Sprintf("%v", d)) +} + +func (v *MockCreateOperations) expectCall(callSignature, output string, err error) { + v.expectedCalls = append(v.expectedCalls, Call{ + signature: callSignature, + output: output, + err: err, + }) +} + +func (v *MockCreateOperations) doCall(callSignature string) (string, error) { + if v.call >= len(v.expectedCalls) { + v.test.Fatal("Unexpected call", callSignature) + + } + + call := v.expectedCalls[v.call] + if call.signature != "IGNORE CALL" && (callSignature != call.signature) { + v.test.Fatal("Unexpected call", callSignature) + } + + v.call++ + + return call.output, call.err +} + +func TestCreateVM(t *testing.T) { + shareName, shareDir := getShareDriveAndName() + + operations := &MockCreateOperations{ + test: t, + expectedCalls: []Call{ + {"CopyIsoToMachineDir path default http://b2d.org", "", nil}, + {"Generate path/machines/default/id_rsa", "", nil}, + {"Create 20000 path/machines/default/id_rsa.pub path/machines/default/disk.vmdk", "", nil}, + {"vbm createvm --basefolder path/machines/default --name default --register", "", nil}, + {"vbm modifyvm default --firmware bios --bioslogofadein off --bioslogofadeout off --bioslogodisplaytime 0 --biosbootmenu disabled --ostype Linux26_64 --cpus 1 --memory 1024 --acpi on --ioapic on --rtcuseutc on --natdnshostresolver1 off --natdnsproxy1 off --cpuhotplug off --pae on --hpet on --hwvirtex on --nestedpaging on --largepages on --vtxvpid on --accelerate3d off --boot1 dvd", "", nil}, + {"vbm modifyvm default --nic1 nat --nictype1 82540EM --cableconnected1 on", "", nil}, + {"vbm storagectl default --name SATA --add sata --hostiocache on", "", nil}, + {"vbm storageattach default --storagectl SATA --port 0 --device 0 --type dvddrive --medium path/machines/default/boot2docker.iso", "", nil}, + {"vbm storageattach default --storagectl SATA --port 1 --device 0 --type hdd --medium path/machines/default/disk.vmdk", "", nil}, + {"vbm guestproperty set default /VirtualBox/GuestAdd/SharedFolders/MountPrefix /", "", nil}, + {"vbm guestproperty set default /VirtualBox/GuestAdd/SharedFolders/MountDir /", "", nil}, + {"vbm sharedfolder add default --name " + shareName + " --hostpath " + shareDir + " --automount", "", nil}, + {"vbm setextradata default VBoxInternal2/SharedFoldersEnableSymlinksCreate/" + shareName + " 1", "", nil}, + }, + } + + driver := NewDriver("default", "path") + driver.Boot2DockerURL = "http://b2d.org" + driver.VBoxManager = operations + driver.b2dUpdater = operations + driver.sshKeyGenerator = operations + driver.diskCreator = operations + driver.logsReader = operations + driver.ipWaiter = operations + driver.randomInter = operations + driver.sleeper = operations + + err := driver.CreateVM() + + assert.NoError(t, err) +} + +func TestStart(t *testing.T) { + operations := &MockCreateOperations{ + test: t, + expectedCalls: []Call{ + {"vbm showvminfo default --machinereadable", `VMState="poweroff"`, nil}, + {"vbm list hostonlyifs", "", nil}, + {"vbm hostonlyif create", "Interface 'VirtualBox Host-Only Ethernet Adapter' was successfully created", nil}, + {"vbm hostonlyif ipconfig VirtualBox Host-Only Ethernet Adapter --ip 192.168.99.1 --netmask 255.255.255.0", "", nil}, + {"vbm list hostonlyifs", ` +Name: VirtualBox Host-Only Ethernet Adapter +GUID: 786f6276-656e-4074-8000-0a0027000000 +DHCP: Disabled +IPAddress: 192.168.99.1 +NetworkMask: 255.255.255.0 +IPV6Address: +IPV6NetworkMaskPrefixLength: 0 +HardwareAddress: 0a:00:27:00:00:00 +MediumType: Ethernet +Status: Up +VBoxNetworkName: HostInterfaceNetworking-VirtualBox Host-Only Ethernet Adapter`, nil}, + {"vbm list dhcpservers", "", nil}, + {"vbm list dhcpservers", "", nil}, + {"vbm dhcpserver add --netname HostInterfaceNetworking-VirtualBox Host-Only Ethernet Adapter --ip 192.168.99.6 --netmask 255.255.255.0 --lowerip 192.168.99.100 --upperip 192.168.99.254 --enable", "", nil}, + {"vbm modifyvm default --nic2 hostonly --nictype2 82540EM --nicpromisc2 deny --hostonlyadapter2 VirtualBox Host-Only Ethernet Adapter --cableconnected2 on", "", nil}, + {"IGNORE CALL", "", nil}, + {"IGNORE CALL", "", nil}, + {"vbm startvm default --type headless", "", nil}, + {"Read path/machines/default/default/Logs/VBox.log", "", nil}, + {"WaitIP", "", nil}, + {"vbm list hostonlyifs", ` +Name: VirtualBox Host-Only Ethernet Adapter +GUID: 786f6276-656e-4074-8000-0a0027000000 +DHCP: Disabled +IPAddress: 192.168.99.1 +NetworkMask: 255.255.255.0 +IPV6Address: +IPV6NetworkMaskPrefixLength: 0 +HardwareAddress: 0a:00:27:00:00:00 +MediumType: Ethernet +Status: Up +VBoxNetworkName: HostInterfaceNetworking-VirtualBox Host-Only Ethernet Adapter`, nil}, + }, + } + + driver := NewDriver("default", "path") + driver.Boot2DockerURL = "http://b2d.org" + driver.VBoxManager = operations + driver.b2dUpdater = operations + driver.sshKeyGenerator = operations + driver.diskCreator = operations + driver.logsReader = operations + driver.ipWaiter = operations + driver.randomInter = operations + driver.sleeper = operations + + err := driver.Start() + + assert.NoError(t, err) +} + +func TestStartWithHostOnlyAdapterCreationBug(t *testing.T) { + operations := &MockCreateOperations{ + test: t, + expectedCalls: []Call{ + {"vbm showvminfo default --machinereadable", `VMState="poweroff"`, nil}, + {"vbm list hostonlyifs", "", nil}, + {"vbm hostonlyif create", "", errors.New("error: Failed to create the host-only adapter")}, + {"vbm list hostonlyifs", "", nil}, + {"vbm list hostonlyifs", ` +Name: VirtualBox Host-Only Ethernet Adapter +GUID: 786f6276-656e-4074-8000-0a0027000000 +DHCP: Disabled +IPAddress: 192.168.99.1 +NetworkMask: 255.255.255.0 +IPV6Address: +IPV6NetworkMaskPrefixLength: 0 +HardwareAddress: 0a:00:27:00:00:00 +MediumType: Ethernet +Status: Up +VBoxNetworkName: HostInterfaceNetworking-VirtualBox Host-Only Ethernet Adapter`, nil}, + {"vbm hostonlyif ipconfig VirtualBox Host-Only Ethernet Adapter --ip 192.168.99.1 --netmask 255.255.255.0", "", nil}, + {"vbm list hostonlyifs", ` +Name: VirtualBox Host-Only Ethernet Adapter +GUID: 786f6276-656e-4074-8000-0a0027000000 +DHCP: Disabled +IPAddress: 192.168.99.1 +NetworkMask: 255.255.255.0 +IPV6Address: +IPV6NetworkMaskPrefixLength: 0 +HardwareAddress: 0a:00:27:00:00:00 +MediumType: Ethernet +Status: Up +VBoxNetworkName: HostInterfaceNetworking-VirtualBox Host-Only Ethernet Adapter`, nil}, + {"vbm list dhcpservers", "", nil}, + {"vbm list dhcpservers", "", nil}, + {"vbm dhcpserver add --netname HostInterfaceNetworking-VirtualBox Host-Only Ethernet Adapter --ip 192.168.99.6 --netmask 255.255.255.0 --lowerip 192.168.99.100 --upperip 192.168.99.254 --enable", "", nil}, + {"vbm modifyvm default --nic2 hostonly --nictype2 82540EM --nicpromisc2 deny --hostonlyadapter2 VirtualBox Host-Only Ethernet Adapter --cableconnected2 on", "", nil}, + {"IGNORE CALL", "", nil}, + {"IGNORE CALL", "", nil}, + {"vbm startvm default --type headless", "", nil}, + {"Read path/machines/default/default/Logs/VBox.log", "", nil}, + {"WaitIP", "", nil}, + {"vbm list hostonlyifs", ` +Name: VirtualBox Host-Only Ethernet Adapter +GUID: 786f6276-656e-4074-8000-0a0027000000 +DHCP: Disabled +IPAddress: 192.168.99.100 +NetworkMask: 255.255.255.0 +IPV6Address: +IPV6NetworkMaskPrefixLength: 0 +HardwareAddress: 0a:00:27:00:00:00 +MediumType: Ethernet +Status: Up +VBoxNetworkName: HostInterfaceNetworking-VirtualBox Host-Only Ethernet Adapter`, nil}, + {"vbm showvminfo default --machinereadable", `VMState="running"`, nil}, + {"vbm controlvm default acpipowerbutton", "", nil}, + {"vbm showvminfo default --machinereadable", `VMState="stopped"`, nil}, + {"Sleep 5s", "", nil}, + {"vbm hostonlyif ipconfig VirtualBox Host-Only Ethernet Adapter --ip 192.168.99.1 --netmask 255.255.255.0", "", nil}, + {"Sleep 5s", "", nil}, + {"vbm startvm default --type headless", "", nil}, + {"WaitIP", "", nil}, + }, + } + + driver := NewDriver("default", "path") + driver.Boot2DockerURL = "http://b2d.org" + driver.VBoxManager = operations + driver.b2dUpdater = operations + driver.sshKeyGenerator = operations + driver.diskCreator = operations + driver.logsReader = operations + driver.ipWaiter = operations + driver.randomInter = operations + driver.sleeper = operations + + err := driver.Start() + + assert.NoError(t, err) +}