package virtualbox import ( "errors" "net" "strings" "testing" "fmt" "time" "github.com/docker/machine/libmachine/drivers" "github.com/docker/machine/libmachine/state" "github.com/stretchr/testify/assert" ) type VBoxManagerMock struct { args string stdOut string stdErr string err error } func (v *VBoxManagerMock) vbm(args ...string) error { _, _, err := v.vbmOutErr(args...) return err } func (v *VBoxManagerMock) vbmOut(args ...string) (string, error) { stdout, _, err := v.vbmOutErr(args...) return stdout, err } 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 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 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) { driver := newTestDriver("default") // test IP 1.2.3.4 testIP := net.IPv4(byte(1), byte(2), byte(3), byte(4)) newIP, err := getRandomIPinSubnet(driver, testIP) if err != nil { t.Fatal(err) } if testIP.Equal(newIP) { t.Fatalf("expected different IP (source %s); received %s", testIP.String(), newIP.String()) } if newIP[0] != testIP[0] { t.Fatalf("expected first octet of %d; received %d", testIP[0], newIP[0]) } if newIP[1] != testIP[1] { t.Fatalf("expected second octet of %d; received %d", testIP[1], newIP[1]) } if newIP[2] != testIP[2] { 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 TestParseValidCIDR(t *testing.T) { ip, network, err := parseAndValidateCIDR("192.168.100.1/24") assert.Equal(t, "192.168.100.1", ip.String()) assert.Equal(t, "192.168.100.0", network.IP.String()) assert.Equal(t, "ffffff00", network.Mask.String()) assert.NoError(t, err) } func TestInvalidCIDR(t *testing.T) { ip, network, err := parseAndValidateCIDR("192.168.100.1") assert.EqualError(t, err, "invalid CIDR address: 192.168.100.1") assert.Nil(t, ip) assert.Nil(t, network) } func TestInvalidNetworkIpCIDR(t *testing.T) { ip, network, err := parseAndValidateCIDR("192.168.100.0/24") assert.Equal(t, ErrNetworkAddrCidr, err) assert.Nil(t, ip) assert.Nil(t, network) } func TestSetConfigFromFlags(t *testing.T) { driver := newTestDriver("default") checkFlags := &drivers.CheckDriverOptions{ FlagsValues: map[string]interface{}{}, CreateFlags: driver.GetCreateFlags(), } err := driver.SetConfigFromFlags(checkFlags) 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) }