diff --git a/drivers/virtualbox/disk.go b/drivers/virtualbox/disk.go index 7fe2467184..966a425565 100644 --- a/drivers/virtualbox/disk.go +++ b/drivers/virtualbox/disk.go @@ -11,6 +11,16 @@ type VirtualDisk struct { Path string } +func (d *Driver) getVMDiskInfo() (*VirtualDisk, error) { + out, err := d.vbmOut("showvminfo", d.MachineName, "--machinereadable") + if err != nil { + return nil, err + } + + r := strings.NewReader(out) + return parseDiskInfo(r) +} + func parseDiskInfo(r io.Reader) (*VirtualDisk, error) { s := bufio.NewScanner(r) disk := &VirtualDisk{} @@ -36,12 +46,3 @@ func parseDiskInfo(r io.Reader) (*VirtualDisk, error) { } return disk, nil } - -func getVMDiskInfo(name string) (*VirtualDisk, error) { - out, err := vbmOut("showvminfo", name, "--machinereadable") - if err != nil { - return nil, err - } - r := strings.NewReader(out) - return parseDiskInfo(r) -} diff --git a/drivers/virtualbox/network.go b/drivers/virtualbox/network.go index bd6f4f1365..a432443c31 100644 --- a/drivers/virtualbox/network.go +++ b/drivers/virtualbox/network.go @@ -31,7 +31,21 @@ type hostOnlyNetwork struct { NetworkName string // referenced in DHCP.NetworkName } -// Config changes the configuration of the host-only network. +// TODO: use VBoxManager.vbm() instead +func vbm(args ...string) error { + vBoxManager := &VBoxCmdManager{} + _, _, err := vBoxManager.vbmOutErr(args...) + return err +} + +// TODO: use VBoxManager.vbmOut() instead +func vbmOut(args ...string) (string, error) { + vBoxManager := &VBoxCmdManager{} + stdout, _, err := vBoxManager.vbmOutErr(args...) + return stdout, err +} + +// Save changes the configuration of the host-only network. func (n *hostOnlyNetwork) Save() error { if n.IPv4.IP != nil && n.IPv4.Mask != nil { if err := vbm("hostonlyif", "ipconfig", n.Name, "--ip", n.IPv4.IP.String(), "--netmask", net.IP(n.IPv4.Mask).String()); err != nil { @@ -66,7 +80,7 @@ func createHostonlyNet() (*hostOnlyNetwork, error) { return &hostOnlyNetwork{Name: res[1]}, nil } -// HostonlyNets gets all host-only networks in a map keyed by HostonlyNet.NetworkName. +// listHostOnlyNetworks gets all host-only networks in a map keyed by HostonlyNet.NetworkName. func listHostOnlyNetworks() (map[string]*hostOnlyNetwork, error) { out, err := vbmOut("list", "hostonlyifs") if err != nil { @@ -211,17 +225,17 @@ func addDHCPServer(kind, name string, d dhcpServer) error { return vbm(args...) } -// AddInternalDHCP adds a DHCP server to an internal network. +// addInternalDHCP adds a DHCP server to an internal network. func addInternalDHCP(netname string, d dhcpServer) error { return addDHCPServer("--netname", netname, d) } -// AddHostonlyDHCP adds a DHCP server to a host-only network. +// addHostonlyDHCP adds a DHCP server to a host-only network. func addHostonlyDHCP(ifname string, d dhcpServer) error { return addDHCPServer("--netname", "HostInterfaceNetworking-"+ifname, d) } -// DHCPs gets all DHCP server settings in a map keyed by DHCP.NetworkName. +// getDHCPServers gets all DHCP server settings in a map keyed by DHCP.NetworkName. func getDHCPServers() (map[string]*dhcpServer, error) { out, err := vbmOut("list", "dhcpservers") if err != nil { diff --git a/drivers/virtualbox/vbm.go b/drivers/virtualbox/vbm.go index c0d4f09c76..e177e31e92 100644 --- a/drivers/virtualbox/vbm.go +++ b/drivers/virtualbox/vbm.go @@ -15,58 +15,40 @@ import ( ) var ( - reVMNameUUID = regexp.MustCompile(`"(.+)" {([0-9a-f-]+)}`) - reVMInfoLine = regexp.MustCompile(`(?:"(.+)"|(.+))=(?:"(.*)"|(.*))`) reColonLine = regexp.MustCompile(`(.+):\s+(.*)`) reEqualLine = regexp.MustCompile(`(.+)=(.*)`) reEqualQuoteLine = regexp.MustCompile(`"(.+)"="(.*)"`) reMachineNotFound = regexp.MustCompile(`Could not find a registered machine named '(.+)'`) -) -var ( - ErrMachineExist = errors.New("machine already exists") ErrMachineNotExist = errors.New("machine does not exist") ErrVBMNotFound = errors.New("VBoxManage not found") - vboxManageCmd = setVBoxManageCmd() + + vboxManageCmd = detectVBoxManageCmd() ) -// detect the VBoxManage cmd's path if needed -func setVBoxManageCmd() string { - cmd := "VBoxManage" - if path, err := exec.LookPath(cmd); err == nil { - return path - } - if runtime.GOOS == "windows" { - if p := os.Getenv("VBOX_INSTALL_PATH"); p != "" { - if path, err := exec.LookPath(filepath.Join(p, cmd)); err == nil { - return path - } - } - if p := os.Getenv("VBOX_MSI_INSTALL_PATH"); p != "" { - if path, err := exec.LookPath(filepath.Join(p, cmd)); err == nil { - return path - } - } - // look at HKEY_LOCAL_MACHINE\SOFTWARE\Oracle\VirtualBox\InstallDir - p := "C:\\Program Files\\Oracle\\VirtualBox" - if path, err := exec.LookPath(filepath.Join(p, cmd)); err == nil { - return path - } - } - return cmd +// VBoxManager defines the interface to communicate to VirtualBox. +type VBoxManager interface { + vbm(args ...string) error + + vbmOut(args ...string) (string, error) + + vbmOutErr(args ...string) (string, string, error) } -func vbm(args ...string) error { - _, _, err := vbmOutErr(args...) +// VBoxCmdManager communicates with VirtualBox through the commandline using `VBoxManage`. +type VBoxCmdManager struct{} + +func (v *VBoxCmdManager) vbm(args ...string) error { + _, _, err := v.vbmOutErr(args...) return err } -func vbmOut(args ...string) (string, error) { - stdout, _, err := vbmOutErr(args...) +func (v *VBoxCmdManager) vbmOut(args ...string) (string, error) { + stdout, _, err := v.vbmOutErr(args...) return stdout, err } -func vbmOutErr(args ...string) (string, string, error) { +func (v *VBoxCmdManager) vbmOutErr(args ...string) (string, string, error) { cmd := exec.Command(vboxManageCmd, args...) log.Debugf("COMMAND: %v %v", vboxManageCmd, strings.Join(args, " ")) var stdout bytes.Buffer @@ -92,3 +74,31 @@ func vbmOutErr(args ...string) (string, string, error) { } return stdout.String(), stderrStr, err } + +// detectVBoxManageCmd detects the VBoxManage cmd's path if needed +func detectVBoxManageCmd() string { + cmd := "VBoxManage" + if path, err := exec.LookPath(cmd); err == nil { + return path + } + + if runtime.GOOS == "windows" { + if p := os.Getenv("VBOX_INSTALL_PATH"); p != "" { + if path, err := exec.LookPath(filepath.Join(p, cmd)); err == nil { + return path + } + } + if p := os.Getenv("VBOX_MSI_INSTALL_PATH"); p != "" { + if path, err := exec.LookPath(filepath.Join(p, cmd)); err == nil { + return path + } + } + // look at HKEY_LOCAL_MACHINE\SOFTWARE\Oracle\VirtualBox\InstallDir + p := "C:\\Program Files\\Oracle\\VirtualBox" + if path, err := exec.LookPath(filepath.Join(p, cmd)); err == nil { + return path + } + } + + return cmd +} diff --git a/drivers/virtualbox/virtualbox.go b/drivers/virtualbox/virtualbox.go index da576cdb34..b838424bb2 100644 --- a/drivers/virtualbox/virtualbox.go +++ b/drivers/virtualbox/virtualbox.go @@ -48,6 +48,7 @@ var ( ) type Driver struct { + VBoxManager *drivers.BaseDriver CPU int Memory int @@ -60,8 +61,10 @@ type Driver struct { NoShare bool } +// NewDriver creates a new VirtualBox driver with default settings. func NewDriver(hostName, storePath string) *Driver { return &Driver{ + VBoxManager: &VBoxCmdManager{}, BaseDriver: &drivers.BaseDriver{ MachineName: hostName, StorePath: storePath, @@ -75,6 +78,8 @@ func NewDriver(hostName, storePath string) *Driver { } } +// GetCreateFlags registers the flags this driver adds to +// "docker hosts create" func (d *Driver) GetCreateFlags() []mcnflag.Flag { return []mcnflag.Flag{ mcnflag.IntFlag{ @@ -176,9 +181,10 @@ func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { return nil } +// PreCreateCheck checks that VBoxManage exists and works func (d *Driver) PreCreateCheck() error { // Check that VBoxManage exists and works - return vbm() + return d.vbm() } // IsVTXDisabled checks if VT-X is disabled in the BIOS. If it is, the vm will fail to start. @@ -250,9 +256,9 @@ func (d *Driver) Create() error { name := d.Boot2DockerImportVM // make sure vm is stopped - _ = vbm("controlvm", name, "poweroff") + _ = d.vbm("controlvm", name, "poweroff") - diskInfo, err := getVMDiskInfo(name) + diskInfo, err := d.getVMDiskInfo() if err != nil { return err } @@ -261,12 +267,12 @@ func (d *Driver) Create() error { return err } - if err := vbm("clonehd", diskInfo.Path, d.diskPath()); err != nil { + if err := d.vbm("clonehd", diskInfo.Path, d.diskPath()); err != nil { return err } log.Debugf("Importing VM settings...") - vmInfo, err := getVMInfo(name) + vmInfo, err := d.getVMInfo() if err != nil { return err } @@ -291,7 +297,7 @@ func (d *Driver) Create() error { } } - if err := vbm("createvm", + if err := d.vbm("createvm", "--basefolder", d.ResolveStorePath("."), "--name", d.MachineName, "--register"); err != nil { @@ -309,7 +315,7 @@ func (d *Driver) Create() error { cpus = 32 } - if err := vbm("modifyvm", d.MachineName, + if err := d.vbm("modifyvm", d.MachineName, "--firmware", "bios", "--bioslogofadein", "off", "--bioslogofadeout", "off", @@ -335,7 +341,7 @@ func (d *Driver) Create() error { return err } - if err := vbm("modifyvm", d.MachineName, + if err := d.vbm("modifyvm", d.MachineName, "--nic1", "nat", "--nictype1", "82540EM", "--cableconnected1", "on"); err != nil { @@ -346,14 +352,14 @@ func (d *Driver) Create() error { return err } - if err := vbm("storagectl", d.MachineName, + if err := d.vbm("storagectl", d.MachineName, "--name", "SATA", "--add", "sata", "--hostiocache", "on"); err != nil { return err } - if err := vbm("storageattach", d.MachineName, + if err := d.vbm("storageattach", d.MachineName, "--storagectl", "SATA", "--port", "0", "--device", "0", @@ -362,7 +368,7 @@ func (d *Driver) Create() error { return err } - if err := vbm("storageattach", d.MachineName, + if err := d.vbm("storageattach", d.MachineName, "--storagectl", "SATA", "--port", "1", "--device", "0", @@ -372,10 +378,10 @@ func (d *Driver) Create() error { } // let VBoxService do nice magic automounting (when it's used) - if err := vbm("guestproperty", "set", d.MachineName, "/VirtualBox/GuestAdd/SharedFolders/MountPrefix", "/"); err != nil { + if err := d.vbm("guestproperty", "set", d.MachineName, "/VirtualBox/GuestAdd/SharedFolders/MountPrefix", "/"); err != nil { return err } - if err := vbm("guestproperty", "set", d.MachineName, "/VirtualBox/GuestAdd/SharedFolders/MountDir", "/"); err != nil { + if err := d.vbm("guestproperty", "set", d.MachineName, "/VirtualBox/GuestAdd/SharedFolders/MountDir", "/"); err != nil { return err } @@ -403,12 +409,12 @@ func (d *Driver) Create() error { } // woo, shareDir exists! let's carry on! - if err := vbm("sharedfolder", "add", d.MachineName, "--name", shareName, "--hostpath", shareDir, "--automount"); err != nil { + if err := d.vbm("sharedfolder", "add", d.MachineName, "--name", shareName, "--hostpath", shareDir, "--automount"); err != nil { return err } // enable symlinks - if err := vbm("setextradata", d.MachineName, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/"+shareName, "1"); err != nil { + if err := d.vbm("setextradata", d.MachineName, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/"+shareName, "1"); err != nil { return err } } @@ -425,12 +431,13 @@ func (d *Driver) hostOnlyIpAvailable() bool { log.Debugf("ERROR getting IP: %s", err) return false } - if ip != "" { - log.Debugf("IP is %s", ip) - return true + if ip == "" { + log.Debug("Strangely, there was no error attempting to get the IP, but it was still empty.") + return false } - log.Debug("Strangely, there was no error attempting to get the IP, but it was still empty.") - return false + + log.Debugf("IP is %s", ip) + return true } func (d *Driver) Start() error { @@ -448,16 +455,16 @@ func (d *Driver) Start() error { switch s { case state.Stopped, state.Saved: - d.SSHPort, err = setPortForwarding(d.MachineName, 1, "ssh", "tcp", 22, d.SSHPort) + d.SSHPort, err = setPortForwarding(d, 1, "ssh", "tcp", 22, d.SSHPort) if err != nil { return err } - if err := vbm("startvm", d.MachineName, "--type", "headless"); err != nil { + if err := d.vbm("startvm", d.MachineName, "--type", "headless"); err != nil { return err } log.Infof("Starting VM...") case state.Paused: - if err := vbm("controlvm", d.MachineName, "resume", "--type", "headless"); err != nil { + if err := d.vbm("controlvm", d.MachineName, "resume", "--type", "headless"); err != nil { return err } log.Infof("Resuming VM ...") @@ -491,7 +498,7 @@ func (d *Driver) Start() error { } func (d *Driver) Stop() error { - if err := vbm("controlvm", d.MachineName, "acpipowerbutton"); err != nil { + if err := d.vbm("controlvm", d.MachineName, "acpipowerbutton"); err != nil { return err } for { @@ -527,7 +534,7 @@ func (d *Driver) Remove() error { } // vbox will not release it's lock immediately after the stop time.Sleep(1 * time.Second) - return vbm("unregistervm", "--delete", d.MachineName) + return d.vbm("unregistervm", "--delete", d.MachineName) } func (d *Driver) Restart() error { @@ -545,11 +552,11 @@ func (d *Driver) Restart() error { } func (d *Driver) Kill() error { - return vbm("controlvm", d.MachineName, "poweroff") + return d.vbm("controlvm", d.MachineName, "poweroff") } func (d *Driver) GetState() (state.State, error) { - stdout, stderr, err := vbmOutErr("showvminfo", d.MachineName, + stdout, stderr, err := d.vbmOutErr("showvminfo", d.MachineName, "--machinereadable") if err != nil { if reMachineNotFound.FindString(stderr) != "" { @@ -694,21 +701,16 @@ func (d *Driver) setupHostOnlyNetwork(machineName string) error { lowerDHCPIP, upperDHCPIP, ) - if err != nil { return err } - if err := vbm("modifyvm", machineName, + return d.vbm("modifyvm", machineName, "--nic2", "hostonly", "--nictype2", d.HostOnlyNicType, "--nicpromisc2", d.HostOnlyPromiscMode, "--hostonlyadapter2", hostOnlyNetwork.Name, - "--cableconnected2", "on"); err != nil { - return err - } - - return nil + "--cableconnected2", "on") } // createDiskImage makes a disk image at dest with the given size in MB. If r is @@ -800,7 +802,7 @@ func getAvailableTCPPort(port int) (int, error) { } // Setup a NAT port forwarding entry. -func setPortForwarding(machine string, interfaceNum int, mapName, protocol string, guestPort, desiredHostPort int) (int, error) { +func setPortForwarding(d *Driver, interfaceNum int, mapName, protocol string, guestPort, desiredHostPort int) (int, error) { actualHostPort, err := getAvailableTCPPort(desiredHostPort) if err != nil { return -1, err @@ -810,8 +812,8 @@ func setPortForwarding(machine string, interfaceNum int, mapName, protocol strin guestPort, mapName, desiredHostPort, actualHostPort) } cmd := fmt.Sprintf("--natpf%d", interfaceNum) - vbm("modifyvm", machine, cmd, "delete", mapName) - if err := vbm("modifyvm", machine, + d.vbm("modifyvm", d.MachineName, cmd, "delete", mapName) + if err := d.vbm("modifyvm", d.MachineName, cmd, fmt.Sprintf("%s,%s,127.0.0.1,%d,,%d", mapName, protocol, actualHostPort, guestPort)); err != nil { return -1, err } diff --git a/drivers/virtualbox/virtualbox_test.go b/drivers/virtualbox/virtualbox_test.go index 5df5e5f4cb..3fb2c7e60d 100644 --- a/drivers/virtualbox/virtualbox_test.go +++ b/drivers/virtualbox/virtualbox_test.go @@ -1,10 +1,102 @@ package virtualbox import ( + "errors" "net" + "strings" "testing" + + "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 { + VBoxCmdManager + args string + stdOut string + stdErr string + err error +} + +func (v *VBoxManagerMock) vbmOutErr(args ...string) (string, string, error) { + if strings.Join(args, " ") == v.args { + return v.stdOut, v.stdErr, v.err + } + return "", "", errors.New("Invalid args") +} + +func TestState(t *testing.T) { + var tests = []struct { + stdOut string + state state.State + }{ + {`VMState="running"`, state.Running}, + {`VMState="paused"`, state.Paused}, + {`VMState="saved"`, state.Saved}, + {`VMState="poweroff"`, state.Stopped}, + {`VMState="aborted"`, state.Stopped}, + {`VMState="whatever"`, state.None}, + {`VMState=`, state.None}, + } + + for _, expected := range tests { + driver := newTestDriver("default") + driver.VBoxManager = &VBoxManagerMock{ + args: "showvminfo default --machinereadable", + stdOut: expected.stdOut, + } + + machineState, err := driver.GetState() + + assert.NoError(t, err) + assert.Equal(t, expected.state, machineState) + } +} + +func TestStateErrors(t *testing.T) { + var tests = []struct { + stdErr string + err error + finalErr error + }{ + {"Could not find a registered machine named 'unknown'", errors.New("Bug"), errors.New("machine does not exist")}, + {"", errors.New("Unexpected error"), errors.New("Unexpected error")}, + } + + for _, expected := range tests { + driver := newTestDriver("default") + driver.VBoxManager = &VBoxManagerMock{ + args: "showvminfo default --machinereadable", + stdErr: expected.stdErr, + err: expected.err, + } + + machineState, err := driver.GetState() + + assert.Equal(t, err, expected.finalErr) + assert.Equal(t, state.Error, machineState) + } +} + func TestGetRandomIPinSubnet(t *testing.T) { // test IP 1.2.3.4 testIP := net.IPv4(byte(1), byte(2), byte(3), byte(4)) @@ -29,3 +121,37 @@ func TestGetRandomIPinSubnet(t *testing.T) { t.Fatalf("expected third octet of %d; received %d", testIP[2], newIP[2]) } } + +func TestGetIPErrors(t *testing.T) { + var tests = []struct { + stdOut string + err error + finalErr error + }{ + {`VMState="poweroff"`, nil, errors.New("Host is not running")}, + {"", errors.New("Unable to get state"), errors.New("Unable to get state")}, + } + + for _, expected := range tests { + driver := newTestDriver("default") + driver.VBoxManager = &VBoxManagerMock{ + args: "showvminfo default --machinereadable", + stdOut: expected.stdOut, + err: expected.err, + } + + ip, err := driver.GetIP() + + assert.Empty(t, ip) + assert.Equal(t, err, expected.finalErr) + + url, err := driver.GetURL() + + assert.Empty(t, url) + assert.Equal(t, err, expected.finalErr) + } +} + +func newTestDriver(name string) *Driver { + return NewDriver(name, "") +} diff --git a/drivers/virtualbox/vm.go b/drivers/virtualbox/vm.go index ff119fae53..b32d75580f 100644 --- a/drivers/virtualbox/vm.go +++ b/drivers/virtualbox/vm.go @@ -12,6 +12,16 @@ type VirtualBoxVM struct { Memory int } +func (d *Driver) getVMInfo() (*VirtualBoxVM, error) { + out, err := d.vbmOut("showvminfo", d.MachineName, "--machinereadable") + if err != nil { + return nil, err + } + + r := strings.NewReader(out) + return parseVMInfo(r) +} + func parseVMInfo(r io.Reader) (*VirtualBoxVM, error) { s := bufio.NewScanner(r) vm := &VirtualBoxVM{} @@ -44,12 +54,3 @@ func parseVMInfo(r io.Reader) (*VirtualBoxVM, error) { } return vm, nil } - -func getVMInfo(name string) (*VirtualBoxVM, error) { - out, err := vbmOut("showvminfo", name, "--machinereadable") - if err != nil { - return nil, err - } - r := strings.NewReader(out) - return parseVMInfo(r) -}