From 8fc260e8b9cebd09ddc10aa1e9924426387772ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Sat, 22 Nov 2025 12:40:06 +0100 Subject: [PATCH 1/3] Move hyperv and virtualbox drivers to minikube Preparing for making the libmachine API internal --- go.mod | 3 +- pkg/drivers/hyperv/hyperv.go | 513 ++++++++ pkg/drivers/hyperv/powershell.go | 114 ++ pkg/drivers/virtualbox/disk.go | 141 +++ pkg/drivers/virtualbox/disk_test.go | 55 + pkg/drivers/virtualbox/ip.go | 36 + pkg/drivers/virtualbox/misc.go | 112 ++ pkg/drivers/virtualbox/network.go | 415 +++++++ pkg/drivers/virtualbox/network_test.go | 402 +++++++ pkg/drivers/virtualbox/vbm.go | 165 +++ pkg/drivers/virtualbox/vbm_test.go | 152 +++ pkg/drivers/virtualbox/virtualbox.go | 1068 +++++++++++++++++ pkg/drivers/virtualbox/virtualbox_darwin.go | 13 + .../virtualbox/virtualbox_darwin_test.go | 15 + pkg/drivers/virtualbox/virtualbox_freebsd.go | 20 + pkg/drivers/virtualbox/virtualbox_linux.go | 13 + .../virtualbox/virtualbox_linux_test.go | 14 + pkg/drivers/virtualbox/virtualbox_openbsd.go | 13 + pkg/drivers/virtualbox/virtualbox_test.go | 758 ++++++++++++ pkg/drivers/virtualbox/virtualbox_windows.go | 105 ++ pkg/drivers/virtualbox/vm.go | 41 + pkg/drivers/virtualbox/vm_test.go | 44 + pkg/drivers/virtualbox/vtx.go | 28 + pkg/drivers/virtualbox/vtx_intel.go | 14 + pkg/drivers/virtualbox/vtx_other.go | 8 + pkg/drivers/virtualbox/vtx_test.go | 72 ++ pkg/minikube/registry/drvs/hyperv/hyperv.go | 2 +- .../registry/drvs/virtualbox/virtualbox.go | 2 +- 28 files changed, 4335 insertions(+), 3 deletions(-) create mode 100644 pkg/drivers/hyperv/hyperv.go create mode 100644 pkg/drivers/hyperv/powershell.go create mode 100644 pkg/drivers/virtualbox/disk.go create mode 100644 pkg/drivers/virtualbox/disk_test.go create mode 100644 pkg/drivers/virtualbox/ip.go create mode 100644 pkg/drivers/virtualbox/misc.go create mode 100644 pkg/drivers/virtualbox/network.go create mode 100644 pkg/drivers/virtualbox/network_test.go create mode 100644 pkg/drivers/virtualbox/vbm.go create mode 100644 pkg/drivers/virtualbox/vbm_test.go create mode 100644 pkg/drivers/virtualbox/virtualbox.go create mode 100644 pkg/drivers/virtualbox/virtualbox_darwin.go create mode 100644 pkg/drivers/virtualbox/virtualbox_darwin_test.go create mode 100644 pkg/drivers/virtualbox/virtualbox_freebsd.go create mode 100644 pkg/drivers/virtualbox/virtualbox_linux.go create mode 100644 pkg/drivers/virtualbox/virtualbox_linux_test.go create mode 100644 pkg/drivers/virtualbox/virtualbox_openbsd.go create mode 100644 pkg/drivers/virtualbox/virtualbox_test.go create mode 100644 pkg/drivers/virtualbox/virtualbox_windows.go create mode 100644 pkg/drivers/virtualbox/vm.go create mode 100644 pkg/drivers/virtualbox/vm_test.go create mode 100644 pkg/drivers/virtualbox/vtx.go create mode 100644 pkg/drivers/virtualbox/vtx_intel.go create mode 100644 pkg/drivers/virtualbox/vtx_other.go create mode 100644 pkg/drivers/virtualbox/vtx_test.go diff --git a/go.mod b/go.mod index 6e3acd7b0a3c..9615358cdf39 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/Parallels/docker-machine-parallels/v2 v2.0.1 github.com/VividCortex/godaemon v1.0.0 github.com/Xuanwo/go-locale v1.1.3 + github.com/aregm/cpuid v0.0.0-20181003105527-1a4a6f06a1c6 github.com/blang/semver/v4 v4.0.0 github.com/briandowns/spinner v1.23.2 github.com/cenkalti/backoff/v4 v4.3.0 @@ -59,6 +60,7 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.9 github.com/spf13/viper v1.20.1 + github.com/stretchr/testify v1.11.1 github.com/zchee/go-vmnet v0.0.0-20161021174912-97ebf9174097 go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/sdk v1.38.0 @@ -102,7 +104,6 @@ require ( github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/VividCortex/ewma v1.2.0 // indirect - github.com/aregm/cpuid v0.0.0-20181003105527-1a4a6f06a1c6 // indirect github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect github.com/aws/aws-sdk-go-v2/config v1.29.15 // indirect diff --git a/pkg/drivers/hyperv/hyperv.go b/pkg/drivers/hyperv/hyperv.go new file mode 100644 index 000000000000..ec248112979e --- /dev/null +++ b/pkg/drivers/hyperv/hyperv.go @@ -0,0 +1,513 @@ +package hyperv + +import ( + "encoding/json" + "fmt" + "io" + "net" + "os" + "strings" + "time" + + "github.com/docker/machine/libmachine/drivers" + "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" +) + +type Driver struct { + *drivers.BaseDriver + Boot2DockerURL string + VSwitch string + DiskSize int + MemSize int + CPU int + MacAddr string + VLanID int + DisableDynamicMemory bool +} + +const ( + defaultDiskSize = 20000 + defaultMemory = 1024 + defaultCPU = 1 + defaultVLanID = 0 + defaultDisableDynamicMemory = false + defaultSwitchID = "c08cb7b8-9b3c-408e-8e30-5e16a3aeb444" +) + +// NewDriver creates a new Hyper-v driver with default settings. +func NewDriver(hostName, storePath string) *Driver { + return &Driver{ + DiskSize: defaultDiskSize, + MemSize: defaultMemory, + CPU: defaultCPU, + DisableDynamicMemory: defaultDisableDynamicMemory, + BaseDriver: &drivers.BaseDriver{ + MachineName: hostName, + StorePath: storePath, + }, + } +} + +// GetCreateFlags registers the flags this driver adds to +// "docker hosts create" +func (d *Driver) GetCreateFlags() []mcnflag.Flag { + return []mcnflag.Flag{ + mcnflag.StringFlag{ + Name: "hyperv-boot2docker-url", + Usage: "URL of the boot2docker ISO. Defaults to the latest available version.", + EnvVar: "HYPERV_BOOT2DOCKER_URL", + }, + mcnflag.StringFlag{ + Name: "hyperv-virtual-switch", + Usage: "Virtual switch name. Defaults to first found.", + EnvVar: "HYPERV_VIRTUAL_SWITCH", + }, + mcnflag.IntFlag{ + Name: "hyperv-disk-size", + Usage: "Maximum size of dynamically expanding disk in MB.", + Value: defaultDiskSize, + EnvVar: "HYPERV_DISK_SIZE", + }, + mcnflag.IntFlag{ + Name: "hyperv-memory", + Usage: "Memory size for host in MB.", + Value: defaultMemory, + EnvVar: "HYPERV_MEMORY", + }, + mcnflag.IntFlag{ + Name: "hyperv-cpu-count", + Usage: "number of CPUs for the machine", + Value: defaultCPU, + EnvVar: "HYPERV_CPU_COUNT", + }, + mcnflag.StringFlag{ + Name: "hyperv-static-macaddress", + Usage: "Hyper-V network adapter's static MAC address.", + EnvVar: "HYPERV_STATIC_MACADDRESS", + }, + mcnflag.IntFlag{ + Name: "hyperv-vlan-id", + Usage: "Hyper-V network adapter's VLAN ID if any", + Value: defaultVLanID, + EnvVar: "HYPERV_VLAN_ID", + }, + mcnflag.BoolFlag{ + Name: "hyperv-disable-dynamic-memory", + Usage: "Disable dynamic memory management setting", + EnvVar: "HYPERV_DISABLE_DYNAMIC_MEMORY", + }, + } +} + +func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { + d.Boot2DockerURL = flags.String("hyperv-boot2docker-url") + d.VSwitch = flags.String("hyperv-virtual-switch") + d.DiskSize = flags.Int("hyperv-disk-size") + d.MemSize = flags.Int("hyperv-memory") + d.CPU = flags.Int("hyperv-cpu-count") + d.MacAddr = flags.String("hyperv-static-macaddress") + d.VLanID = flags.Int("hyperv-vlan-id") + d.SSHUser = "docker" + d.DisableDynamicMemory = flags.Bool("hyperv-disable-dynamic-memory") + d.SetSwarmConfigFromFlags(flags) + + return nil +} + +func (d *Driver) GetSSHHostname() (string, error) { + return d.GetIP() +} + +// DriverName returns the name of the driver +func (d *Driver) DriverName() string { + return "hyperv" +} + +func (d *Driver) GetURL() (string, error) { + ip, err := d.GetIP() + if err != nil { + return "", err + } + + if ip == "" { + return "", nil + } + + return fmt.Sprintf("tcp://%s", net.JoinHostPort(ip, "2376")), nil +} + +func (d *Driver) GetState() (state.State, error) { + stdout, err := cmdOut("(", "Hyper-V\\Get-VM", d.MachineName, ").state") + if err != nil { + return state.None, fmt.Errorf("Failed to find the VM status") + } + + resp := parseLines(stdout) + if len(resp) < 1 { + return state.None, nil + } + + switch resp[0] { + case "Running": + return state.Running, nil + case "Off": + return state.Stopped, nil + default: + return state.None, nil + } +} + +// PreCreateCheck checks that the machine creation process can be started safely. +func (d *Driver) PreCreateCheck() error { + // Check that powershell was found + if powershell == "" { + return ErrPowerShellNotFound + } + + // Check that hyperv is installed + if err := hypervAvailable(); err != nil { + return err + } + + // Check that the user is an Administrator + isAdmin, err := isAdministrator() + if err != nil { + return err + } + if !isAdmin { + return ErrNotAdministrator + } + + // Check that there is a virtual switch already configured + if _, err := d.chooseVirtualSwitch(); err != nil { + return err + } + + // 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) + err = b2dutils.UpdateISOCache(d.Boot2DockerURL) + return err +} + +func (d *Driver) Create() error { + b2dutils := mcnutils.NewB2dUtils(d.StorePath) + if err := b2dutils.CopyIsoToMachineDir(d.Boot2DockerURL, d.MachineName); err != nil { + return err + } + + log.Infof("Creating SSH key...") + if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil { + return err + } + + log.Infof("Creating VM...") + if d.VSwitch == "" { + defaultVSwitch, err := d.chooseVirtualSwitch() + if err != nil { + return err + } + d.VSwitch = defaultVSwitch + } + log.Infof("Using switch %q", d.VSwitch) + + diskImage, err := d.generateDiskImage() + if err != nil { + return err + } + + if err := cmd("Hyper-V\\New-VM", + d.MachineName, + "-Path", fmt.Sprintf("'%s'", d.ResolveStorePath(".")), + "-SwitchName", quote(d.VSwitch), + "-MemoryStartupBytes", toMb(d.MemSize)); err != nil { + return err + } + if d.DisableDynamicMemory { + if err := cmd("Hyper-V\\Set-VMMemory", + "-VMName", d.MachineName, + "-DynamicMemoryEnabled", "$false"); err != nil { + return err + } + } + + if d.CPU > 1 { + if err := cmd("Hyper-V\\Set-VMProcessor", + d.MachineName, + "-Count", fmt.Sprintf("%d", d.CPU)); err != nil { + return err + } + } + + if d.MacAddr != "" { + if err := cmd("Hyper-V\\Set-VMNetworkAdapter", + "-VMName", d.MachineName, + "-StaticMacAddress", fmt.Sprintf("\"%s\"", d.MacAddr)); err != nil { + return err + } + } + + if d.VLanID > 0 { + if err := cmd("Hyper-V\\Set-VMNetworkAdapterVlan", + "-VMName", d.MachineName, + "-Access", + "-VlanId", fmt.Sprintf("%d", d.VLanID)); err != nil { + return err + } + } + + if err := cmd("Hyper-V\\Set-VMDvdDrive", + "-VMName", d.MachineName, + "-Path", quote(d.ResolveStorePath("boot2docker.iso"))); err != nil { + return err + } + + if err := cmd("Hyper-V\\Add-VMHardDiskDrive", + "-VMName", d.MachineName, + "-Path", quote(diskImage)); err != nil { + return err + } + + log.Infof("Starting VM...") + return d.Start() +} + +func (d *Driver) chooseVirtualSwitch() (string, error) { + type Switch struct { + ID string + Name string + SwitchType int + } + + getHyperVSwitches := func(filters []string) ([]Switch, error) { + cmd := []string{"Hyper-V\\Get-VMSwitch", "Select Id, Name, SwitchType"} + cmd = append(cmd, filters...) + stdout, err := cmdOut(fmt.Sprintf("[Console]::OutputEncoding = [Text.Encoding]::UTF8; ConvertTo-Json @(%s)", strings.Join(cmd, "|"))) + if err != nil { + return nil, err + } + + var switches []Switch + err = json.Unmarshal([]byte(strings.NewReplacer("\r", "").Replace(stdout)), &switches) + if err != nil { + return nil, err + } + + return switches, nil + } + + if d.VSwitch == "" { + // prefer Default Switch over external switches + switches, err := getHyperVSwitches([]string{fmt.Sprintf("Where-Object {($_.SwitchType -eq 'External') -or ($_.Id -eq '%s')}", defaultSwitchID), "Sort-Object -Property SwitchType"}) + if err != nil { + return "", fmt.Errorf("unable to get available hyperv switches") + } + + if len(switches) < 1 { + return "", fmt.Errorf("no External vswitch nor Default Switch found. A valid vswitch must be available for this command to run. Check https://docs.docker.com/machine/drivers/hyper-v/") + } + + return switches[0].Name, nil + } + + // prefer external switches (using descending order) + switches, err := getHyperVSwitches([]string{fmt.Sprintf("Where-Object {$_.Name -eq '%s'}", d.VSwitch), "Sort-Object -Property SwitchType -Descending"}) + if err != nil { + return "", fmt.Errorf("unable to get available hyperv switches") + } + + if len(switches) < 1 { + return "", fmt.Errorf("vswitch %q not found", d.VSwitch) + } + + return switches[0].Name, nil +} + +// waitForIP waits until the host has a valid IP +func (d *Driver) waitForIP() string { + log.Infof("Waiting for host to start...") + + for { + ip, _ := d.GetIP() + if ip != "" { + return ip + } + + time.Sleep(1 * time.Second) + } +} + +// waitStopped waits until the host is stopped +func (d *Driver) waitStopped() error { + log.Infof("Waiting for host to stop...") + + for { + s, err := d.GetState() + if err != nil { + return err + } + + if s != state.Running { + return nil + } + + time.Sleep(1 * time.Second) + } +} + +// Start starts an host +func (d *Driver) Start() error { + if err := cmd("Hyper-V\\Start-VM", d.MachineName); err != nil { + return err + } + + ip := d.waitForIP() + + d.IPAddress = ip + + return nil +} + +// Stop stops an host +func (d *Driver) Stop() error { + if err := cmd("Hyper-V\\Stop-VM", d.MachineName); err != nil { + return err + } + + if err := d.waitStopped(); err != nil { + return err + } + + d.IPAddress = "" + + return nil +} + +// Remove removes an host +func (d *Driver) Remove() error { + s, err := d.GetState() + if err != nil { + return err + } + + if s == state.Running { + if err := d.Kill(); err != nil { + return err + } + } + + return cmd("Hyper-V\\Remove-VM", d.MachineName, "-Force") +} + +// Restart stops and starts an host +func (d *Driver) Restart() error { + err := d.Stop() + if err != nil { + return err + } + + return d.Start() +} + +// Kill force stops an host +func (d *Driver) Kill() error { + if err := cmd("Hyper-V\\Stop-VM", d.MachineName, "-TurnOff"); err != nil { + return err + } + + if err := d.waitStopped(); err != nil { + return err + } + + d.IPAddress = "" + + return nil +} + +func (d *Driver) GetIP() (string, error) { + s, err := d.GetState() + if err != nil { + return "", err + } + if s != state.Running { + return "", drivers.ErrHostIsNotRunning + } + + stdout, err := cmdOut("((", "Hyper-V\\Get-VM", d.MachineName, ").networkadapters[0]).ipaddresses[0]") + if err != nil { + return "", err + } + + resp := parseLines(stdout) + if len(resp) < 1 { + return "", fmt.Errorf("IP not found") + } + + return resp[0], nil +} + +func (d *Driver) publicSSHKeyPath() string { + return d.GetSSHKeyPath() + ".pub" +} + +// generateDiskImage creates a small fixed vhd, put the tar in, convert to dynamic, then resize +func (d *Driver) generateDiskImage() (string, error) { + diskImage := d.ResolveStorePath("disk.vhd") + fixed := d.ResolveStorePath("fixed.vhd") + + // Resizing vhds requires administrator privileges + // incase the user is only a hyper-v admin then create the disk at the target size to avoid resizing. + isWindowsAdmin, err := isWindowsAdministrator() + if err != nil { + return "", err + } + fixedDiskSize := "10MB" + if !isWindowsAdmin { + fixedDiskSize = toMb(d.DiskSize) + } + + log.Infof("Creating VHD") + if err := cmd("Hyper-V\\New-VHD", "-Path", quote(fixed), "-SizeBytes", fixedDiskSize, "-Fixed"); err != nil { + return "", err + } + + tarBuf, err := mcnutils.MakeDiskImage(d.publicSSHKeyPath()) + if err != nil { + return "", err + } + + file, err := os.OpenFile(fixed, os.O_WRONLY, 0644) + if err != nil { + return "", err + } + defer file.Close() + + _, err = file.Seek(0, io.SeekStart) + if err != nil { + return "", err + } + _, err = file.Write(tarBuf.Bytes()) + if err != nil { + return "", err + } + err = file.Close() + if err != nil { + return "", err + } + + if err := cmd("Hyper-V\\Convert-VHD", "-Path", quote(fixed), "-DestinationPath", quote(diskImage), "-VHDType", "Dynamic", "-DeleteSource"); err != nil { + return "", err + } + + if isWindowsAdmin { + if err := cmd("Hyper-V\\Resize-VHD", "-Path", quote(diskImage), "-SizeBytes", toMb(d.DiskSize)); err != nil { + return "", err + } + } + + return diskImage, nil +} diff --git a/pkg/drivers/hyperv/powershell.go b/pkg/drivers/hyperv/powershell.go new file mode 100644 index 000000000000..b619109520b5 --- /dev/null +++ b/pkg/drivers/hyperv/powershell.go @@ -0,0 +1,114 @@ +package hyperv + +import ( + "bufio" + "bytes" + "errors" + "os/exec" + "strings" + + "fmt" + + "github.com/docker/machine/libmachine/log" +) + +var powershell string + +var ( + ErrPowerShellNotFound = errors.New("Powershell was not found in the path") + ErrNotAdministrator = errors.New("Hyper-v commands have to be run as an Administrator") + ErrNotInstalled = errors.New("Hyper-V PowerShell Module is not available") +) + +func init() { + powershell, _ = exec.LookPath("powershell.exe") +} + +func cmdOut(args ...string) (string, error) { + args = append([]string{"-NoProfile", "-NonInteractive"}, args...) + cmd := exec.Command(powershell, args...) + log.Debugf("[executing ==>] : %v %v", powershell, strings.Join(args, " ")) + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + log.Debugf("[stdout =====>] : %s", stdout.String()) + log.Debugf("[stderr =====>] : %s", stderr.String()) + return stdout.String(), err +} + +func cmd(args ...string) error { + _, err := cmdOut(args...) + return err +} + +func parseLines(stdout string) []string { + resp := []string{} + + s := bufio.NewScanner(strings.NewReader(stdout)) + for s.Scan() { + resp = append(resp, s.Text()) + } + + return resp +} + +func hypervAvailable() error { + stdout, err := cmdOut("@(Get-Module -ListAvailable hyper-v).Name | Get-Unique") + if err != nil { + return err + } + + resp := parseLines(stdout) + if len(resp) == 0 || resp[0] != "Hyper-V" { + return ErrNotInstalled + } + + return nil +} + +func isAdministrator() (bool, error) { + hypervAdmin := isHypervAdministrator() + + if hypervAdmin { + return true, nil + } + + windowsAdmin, err := isWindowsAdministrator() + + if err != nil { + return false, err + } + + return windowsAdmin, nil +} + +func isHypervAdministrator() bool { + stdout, err := cmdOut(`@([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(([System.Security.Principal.SecurityIdentifier]::new("S-1-5-32-578")))`) + if err != nil { + log.Debug(err) + return false + } + + resp := parseLines(stdout) + return resp[0] == "True" +} + +func isWindowsAdministrator() (bool, error) { + stdout, err := cmdOut(`@([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")`) + if err != nil { + return false, err + } + + resp := parseLines(stdout) + return resp[0] == "True", nil +} + +func quote(text string) string { + return fmt.Sprintf("'%s'", text) +} + +func toMb(value int) string { + return fmt.Sprintf("%dMB", value) +} diff --git a/pkg/drivers/virtualbox/disk.go b/pkg/drivers/virtualbox/disk.go new file mode 100644 index 000000000000..82f064f6f395 --- /dev/null +++ b/pkg/drivers/virtualbox/disk.go @@ -0,0 +1,141 @@ +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 { + return nil, err + } + + disk := &VirtualDisk{} + + err = parseKeyValues(out, reEqualQuoteLine, func(key, val string) error { + switch key { + case "SATA-1-0": + disk.Path = val + case "SATA-ImageUUID-1-0": + disk.UUID = val + } + + return nil + }) + if err != nil { + return nil, err + } + + return disk, nil +} diff --git a/pkg/drivers/virtualbox/disk_test.go b/pkg/drivers/virtualbox/disk_test.go new file mode 100644 index 000000000000..b6c591956e66 --- /dev/null +++ b/pkg/drivers/virtualbox/disk_test.go @@ -0,0 +1,55 @@ +package virtualbox + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +const stdOutDiskInfo = ` +storagecontrollerbootable0="on" +"SATA-0-0"="/home/ehazlett/.boot2docker/boot2docker.iso" +"SATA-IsEjected"="off" +"SATA-1-0"="/home/ehazlett/vm/test/disk.vmdk" +"SATA-ImageUUID-1-0"="12345-abcdefg" +"SATA-2-0"="none" +nic1="nat"` + +func TestVMDiskInfo(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "showvminfo default --machinereadable", + stdOut: stdOutDiskInfo, + } + + disk, err := getVMDiskInfo("default", vbox) + + assert.Equal(t, "/home/ehazlett/vm/test/disk.vmdk", disk.Path) + assert.Equal(t, "12345-abcdefg", disk.UUID) + assert.NoError(t, err) +} + +func TestVMDiskInfoError(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "showvminfo default --machinereadable", + err: errors.New("BUG"), + } + + disk, err := getVMDiskInfo("default", vbox) + + assert.Nil(t, disk) + assert.EqualError(t, err, "BUG") +} + +func TestVMDiskInfoInvalidOutput(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "showvminfo default --machinereadable", + stdOut: "INVALID", + } + + disk, err := getVMDiskInfo("default", vbox) + + assert.Empty(t, disk.Path) + assert.Empty(t, disk.UUID) + assert.NoError(t, err) +} diff --git a/pkg/drivers/virtualbox/ip.go b/pkg/drivers/virtualbox/ip.go new file mode 100644 index 000000000000..be22a4fb7a9e --- /dev/null +++ b/pkg/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/pkg/drivers/virtualbox/misc.go b/pkg/drivers/virtualbox/misc.go new file mode 100644 index 000000000000..3972a95443f8 --- /dev/null +++ b/pkg/drivers/virtualbox/misc.go @@ -0,0 +1,112 @@ +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 b2d. +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 { + src := rand.NewSource(time.Now().UnixNano()) + + return &defaultRandomInter{ + rand: rand.New(src), + } +} + +type defaultRandomInter struct { + rand *rand.Rand +} + +func (d *defaultRandomInter) RandomInt(n int) int { + return d.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/pkg/drivers/virtualbox/network.go b/pkg/drivers/virtualbox/network.go new file mode 100644 index 000000000000..633bf574d002 --- /dev/null +++ b/pkg/drivers/virtualbox/network.go @@ -0,0 +1,415 @@ +package virtualbox + +import ( + "errors" + "fmt" + "net" + "regexp" + "strings" + "time" + + "runtime" + + "github.com/docker/machine/libmachine/log" +) + +const ( + buggyNetmask = "0f000000" + dhcpPrefix = "HostInterfaceNetworking-" +) + +var ( + reHostOnlyAdapterCreated = regexp.MustCompile(`Interface '(.+)' was successfully created`) + errNewHostOnlyAdapterNotVisible = errors.New("The host-only adapter we just created is not visible. This is a well known VirtualBox bug. You might want to uninstall it and reinstall at least version 5.0.12 that is is supposed to fix this issue") +) + +// Host-only network. +type hostOnlyNetwork struct { + Name string + GUID string + DHCP bool + IPv4 net.IPNet + HwAddr net.HardwareAddr + Medium string + Status string + NetworkName string // referenced in DHCP.NetworkName +} + +// HostInterfaces returns host network interface info. By default delegates to net.Interfaces() +type HostInterfaces interface { + Interfaces() ([]net.Interface, error) + Addrs(iface *net.Interface) ([]net.Addr, error) +} + +func NewHostInterfaces() HostInterfaces { + return &defaultHostInterfaces{} +} + +type defaultHostInterfaces struct { +} + +func (ni *defaultHostInterfaces) Interfaces() ([]net.Interface, error) { + return net.Interfaces() +} + +func (ni *defaultHostInterfaces) Addrs(iface *net.Interface) ([]net.Addr, error) { + return iface.Addrs() +} + +// Save changes the configuration of the host-only network. +func (n *hostOnlyNetwork) Save(vbox VBoxManager) error { + if err := n.SaveIPv4(vbox); err != nil { + return err + } + + if n.DHCP { + _ = vbox.vbm("hostonlyif", "ipconfig", n.Name, "--dhcp") // not implemented as of VirtualBox 4.3 + } + + return nil +} + +// SaveIPv4 changes the ipv4 configuration of the host-only network. +func (n *hostOnlyNetwork) SaveIPv4(vbox VBoxManager) error { + if n.IPv4.IP != nil && n.IPv4.Mask != nil { + if runtime.GOOS == "windows" { + log.Warn("Windows might ask for the permission to configure a network adapter. Sometimes, such confirmation window is minimized in the taskbar.") + } + + if err := vbox.vbm("hostonlyif", "ipconfig", n.Name, "--ip", n.IPv4.IP.String(), "--netmask", net.IP(n.IPv4.Mask).String()); err != nil { + return err + } + } + + return nil +} + +// createHostonlyAdapter creates a new host-only network. +func createHostonlyAdapter(vbox VBoxManager) (*hostOnlyNetwork, error) { + if runtime.GOOS == "windows" { + log.Warn("Windows might ask for the permission to create a network adapter. Sometimes, such confirmation window is minimized in the taskbar.") + } + + out, err := vbox.vbmOut("hostonlyif", "create") + if err != nil { + return nil, err + } + + res := reHostOnlyAdapterCreated.FindStringSubmatch(out) + if res == nil { + return nil, errors.New("Failed to create host-only adapter") + } + + return &hostOnlyNetwork{Name: res[1]}, nil +} + +// listHostOnlyAdapters gets all host-only adapters in a map keyed by NetworkName. +func listHostOnlyAdapters(vbox VBoxManager) (map[string]*hostOnlyNetwork, error) { + out, err := vbox.vbmOut("list", "hostonlyifs") + if err != nil { + return nil, err + } + + byName := map[string]*hostOnlyNetwork{} + byIP := map[string]*hostOnlyNetwork{} + n := &hostOnlyNetwork{} + + err = parseKeyValues(out, reColonLine, func(key, val string) error { + switch key { + case "Name": + n.Name = val + case "GUID": + n.GUID = val + case "DHCP": + n.DHCP = (val != "Disabled") + case "IPAddress": + n.IPv4.IP = net.ParseIP(val) + case "NetworkMask": + n.IPv4.Mask = parseIPv4Mask(val) + case "HardwareAddress": + mac, err := net.ParseMAC(val) + if err != nil { + return err + } + n.HwAddr = mac + case "MediumType": + n.Medium = val + case "Status": + n.Status = val + case "VBoxNetworkName": + n.NetworkName = val + + if _, present := byName[n.NetworkName]; present { + return fmt.Errorf("VirtualBox is configured with multiple host-only adapters with the same name %q. Please remove one", n.NetworkName) + } + byName[n.NetworkName] = n + + if len(n.IPv4.IP) != 0 { + if _, present := byIP[n.IPv4.IP.String()]; present { + return fmt.Errorf("VirtualBox is configured with multiple host-only adapters with the same IP %q. Please remove one", n.IPv4.IP) + } + byIP[n.IPv4.IP.String()] = n + } + + n = &hostOnlyNetwork{} + } + + return nil + }) + if err != nil { + return nil, err + } + + return byName, nil +} + +func getHostOnlyAdapter(nets map[string]*hostOnlyNetwork, hostIP net.IP, netmask net.IPMask) *hostOnlyNetwork { + for _, n := range nets { + // Second part of this conditional handles a race where + // VirtualBox returns us the incorrect netmask value for the + // newly created adapter. + if hostIP.Equal(n.IPv4.IP) && + (netmask.String() == n.IPv4.Mask.String() || n.IPv4.Mask.String() == buggyNetmask) { + log.Debugf("Found: %s", n.Name) + return n + } + } + + log.Debug("Not found") + return nil +} + +func getOrCreateHostOnlyNetwork(hostIP net.IP, netmask net.IPMask, nets map[string]*hostOnlyNetwork, vbox VBoxManager) (*hostOnlyNetwork, error) { + // Search for an existing host-only adapter. + hostOnlyAdapter := getHostOnlyAdapter(nets, hostIP, netmask) + if hostOnlyAdapter != nil { + return hostOnlyAdapter, nil + } + + // No existing host-only adapter found. Create a new one. + _, err := createHostonlyAdapter(vbox) + if err != nil { + // Sometimes the host-only adapter fails to create. See https://www.virtualbox.org/ticket/14040 + // BUT, it is created in fact! So let's wait until it appears last in the list + log.Warnf("Creating a new host-only adapter produced an error: %s", err) + log.Warn("This is a known VirtualBox bug. Let's try to recover anyway...") + } + + // It can take some time for an adapter to appear. Let's poll. + hostOnlyAdapter, err = waitForNewHostOnlyNetwork(nets, vbox) + if err != nil { + // Sometimes, Vbox says it created it but then it cannot be found... + return nil, errNewHostOnlyAdapterNotVisible + } + + log.Warnf("Found a new host-only adapter: %q", hostOnlyAdapter.Name) + + hostOnlyAdapter.IPv4.IP = hostIP + hostOnlyAdapter.IPv4.Mask = netmask + if err := hostOnlyAdapter.Save(vbox); err != nil { + return nil, err + } + + return hostOnlyAdapter, nil +} + +func waitForNewHostOnlyNetwork(oldNets map[string]*hostOnlyNetwork, vbox VBoxManager) (*hostOnlyNetwork, error) { + for i := 0; i < 10; i++ { + time.Sleep(1 * time.Second) + + newNets, err := listHostOnlyAdapters(vbox) + if err != nil { + return nil, err + } + + for name, latestNet := range newNets { + if _, present := oldNets[name]; !present { + return latestNet, nil + } + } + } + + return nil, errors.New("Failed to find a new host-only adapter") +} + +// DHCP server info. +type dhcpServer struct { + NetworkName string + IPv4 net.IPNet + LowerIP net.IP + UpperIP net.IP + Enabled bool +} + +// removeOrphanDHCPServers removed the DHCP servers linked to no host-only adapter +func removeOrphanDHCPServers(vbox VBoxManager) error { + dhcps, err := listDHCPServers(vbox) + if err != nil { + return err + } + + if len(dhcps) == 0 { + return nil + } + + log.Debug("Removing orphan DHCP servers...") + + nets, err := listHostOnlyAdapters(vbox) + if err != nil { + return err + } + + for name := range dhcps { + if strings.HasPrefix(name, dhcpPrefix) { + if _, present := nets[name]; !present { + if err := vbox.vbm("dhcpserver", "remove", "--netname", name); err != nil { + log.Warnf("Unable to remove orphan dhcp server %q: %s", name, err) + } + } + } + } + + return nil +} + +// addHostOnlyDHCPServer adds a DHCP server to a host-only network. +func addHostOnlyDHCPServer(ifname string, d dhcpServer, vbox VBoxManager) error { + name := dhcpPrefix + ifname + + dhcps, err := listDHCPServers(vbox) + if err != nil { + return err + } + + // On some platforms (OSX), creating a host-only adapter adds a default dhcpserver, + // while on others (Windows?) it does not. + command := "add" + if dhcp, ok := dhcps[name]; ok { + command = "modify" + if (dhcp.IPv4.IP.Equal(d.IPv4.IP)) && (dhcp.IPv4.Mask.String() == d.IPv4.Mask.String()) && (dhcp.LowerIP.Equal(d.LowerIP)) && (dhcp.UpperIP.Equal(d.UpperIP)) && (dhcp.Enabled == d.Enabled) { + // dhcp is up to date + return nil + } + } + + args := []string{"dhcpserver", command, + "--netname", name, + "--ip", d.IPv4.IP.String(), + "--netmask", net.IP(d.IPv4.Mask).String(), + "--lowerip", d.LowerIP.String(), + "--upperip", d.UpperIP.String(), + } + if d.Enabled { + args = append(args, "--enable") + } else { + args = append(args, "--disable") + } + + if runtime.GOOS == "windows" { + log.Warn("Windows might ask for the permission to configure a dhcp server. Sometimes, such confirmation window is minimized in the taskbar.") + } + + return vbox.vbm(args...) +} + +// listDHCPServers lists all DHCP server settings in a map keyed by DHCP.NetworkName. +func listDHCPServers(vbox VBoxManager) (map[string]*dhcpServer, error) { + out, err := vbox.vbmOut("list", "dhcpservers") + if err != nil { + return nil, err + } + + m := map[string]*dhcpServer{} + dhcp := &dhcpServer{} + + err = parseKeyValues(out, reColonLine, func(key, val string) error { + switch key { + case "NetworkName": + dhcp = &dhcpServer{} + m[val] = dhcp + dhcp.NetworkName = val + case "IP": + dhcp.IPv4.IP = net.ParseIP(val) + case "upperIPAddress": + dhcp.UpperIP = net.ParseIP(val) + case "lowerIPAddress": + dhcp.LowerIP = net.ParseIP(val) + case "NetworkMask": + dhcp.IPv4.Mask = parseIPv4Mask(val) + case "Enabled": + dhcp.Enabled = (val == "Yes") + } + + return nil + }) + if err != nil { + return nil, err + } + + return m, nil +} + +// listHostInterfaces returns a map of net.IPNet addresses of host interfaces that are "UP" and not loopback adapters +// and not virtualbox host-only networks (given by excludeNets), keyed by CIDR string. +func listHostInterfaces(hif HostInterfaces, excludeNets map[string]*hostOnlyNetwork) (map[string]*net.IPNet, error) { + ifaces, err := hif.Interfaces() + if err != nil { + return nil, err + } + m := map[string]*net.IPNet{} + + for _, iface := range ifaces { + addrs, err := hif.Addrs(&iface) + if err != nil { + return nil, err + } + + // Check if an address of the interface is in the list of excluded addresses + ifaceExcluded := false + for _, a := range addrs { + switch ipnet := a.(type) { //nolint:gocritic + case *net.IPNet: + _, excluded := excludeNets[ipnet.String()] + if excluded { + ifaceExcluded = true + break + } + } + } + + // If excluded, or not up, or a loopback interface, skip the interface + if ifaceExcluded || iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + continue + } + + // This is a host interface, so add all its addresses to the map + for _, a := range addrs { + switch ipnet := a.(type) { //nolint:gocritic + case *net.IPNet: + m[ipnet.String()] = ipnet + } + } + } + return m, nil +} + +// checkIPNetCollision returns true if any host interfaces conflict with the host-only network mask passed as a parameter. +// This works with IPv4 or IPv6 ip addresses. +func checkIPNetCollision(hostonly *net.IPNet, hostIfaces map[string]*net.IPNet) bool { + for _, ifaceNet := range hostIfaces { + if hostonly.IP.Equal(ifaceNet.IP.Mask(ifaceNet.Mask)) { + return true + } + } + return false +} + +// parseIPv4Mask parses IPv4 netmask written in IP form (e.g. 255.255.255.0). +// This function should really belong to the net package. +func parseIPv4Mask(s string) net.IPMask { + mask := net.ParseIP(s) + if mask == nil { + return nil + } + return net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]) +} diff --git a/pkg/drivers/virtualbox/network_test.go b/pkg/drivers/virtualbox/network_test.go new file mode 100644 index 000000000000..f2569536ed69 --- /dev/null +++ b/pkg/drivers/virtualbox/network_test.go @@ -0,0 +1,402 @@ +package virtualbox + +import ( + "net" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + stdOutOneHostOnlyNetwork = ` +Name: vboxnet0 +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-vboxnet0 + +` + stdOutTwoHostOnlyNetwork = ` +Name: vboxnet0 +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-vboxnet0 + +Name: vboxnet1 +GUID: 786f6276-656e-4174-8000-0a0027000001 +DHCP: Disabled +IPAddress: 169.254.37.187 +NetworkMask: 255.255.255.0 +IPV6Address: +IPV6NetworkMaskPrefixLength: 0 +HardwareAddress: 0a:00:27:00:00:01 +MediumType: Ethernet +Status: Up +VBoxNetworkName: HostInterfaceNetworking-vboxnet1 +` + stdOutListTwoDHCPServers = ` +NetworkName: HostInterfaceNetworking-vboxnet0 +IP: 192.168.99.6 +NetworkMask: 255.255.255.0 +lowerIPAddress: 192.168.99.100 +upperIPAddress: 192.168.99.254 +Enabled: Yes + +NetworkName: HostInterfaceNetworking-vboxnet1 +IP: 192.168.99.7 +NetworkMask: 255.255.255.0 +lowerIPAddress: 192.168.99.100 +upperIPAddress: 192.168.99.254 +Enabled: No +` +) + +type mockHostInterfaces struct { + mockIfaces []net.Interface + mockAddrs map[string]net.Addr +} + +func newMockHostInterfaces() *mockHostInterfaces { + return &mockHostInterfaces{ + mockAddrs: make(map[string]net.Addr), + } +} + +func (mhi *mockHostInterfaces) Interfaces() ([]net.Interface, error) { + return mhi.mockIfaces, nil +} + +func (mhi *mockHostInterfaces) Addrs(iface *net.Interface) ([]net.Addr, error) { + return []net.Addr{mhi.mockAddrs[iface.Name]}, nil +} + +func (mhi *mockHostInterfaces) addMockIface(ip string, mask int, iplen int, name string, flags net.Flags) (*net.IPNet, error) { + iface := &net.Interface{Name: name, Flags: flags} + mhi.mockIfaces = append(mhi.mockIfaces, *iface) + + ipnet := &net.IPNet{IP: net.ParseIP(ip), Mask: net.CIDRMask(mask, 8*iplen)} + if ipnet.IP == nil { + return nil, &net.ParseError{Type: "IP address", Text: ip} + } + mhi.mockAddrs[name] = ipnet + return ipnet, nil +} + +// Tests that when we have a host only network which matches our expectations, +// it gets returned correctly. +func TestGetHostOnlyNetworkHappy(t *testing.T) { + cidr := "192.168.99.0/24" + ip, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + t.Fatalf("Error parsing cidr: %s", err) + } + expectedHostOnlyNetwork := &hostOnlyNetwork{ + IPv4: *ipnet, + } + vboxNets := map[string]*hostOnlyNetwork{ + "HostInterfaceNetworking-vboxnet0": expectedHostOnlyNetwork, + } + + n := getHostOnlyAdapter(vboxNets, ip, ipnet.Mask) + if !reflect.DeepEqual(n, expectedHostOnlyNetwork) { + t.Fatalf("Expected result of calling getHostOnlyNetwork to be the same as expected but it was not:\nexpected: %+v\nactual: %+v\n", expectedHostOnlyNetwork, n) + } +} + +// Tests that we are able to properly detect when a host only network which +// matches our expectations can not be found. +func TestGetHostOnlyNetworkNotFound(t *testing.T) { + // Note that this has a different ip is different from "ip" below. + cidr := "192.168.99.0/24" + _, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + t.Fatalf("Error parsing cidr: %s", err) + } + + ip := net.ParseIP("192.168.59.0").To4() + + // Suppose a vbox net is created, but it doesn't align with our + // expectation. + vboxNet := &hostOnlyNetwork{ + IPv4: *ipnet, + } + vboxNets := map[string]*hostOnlyNetwork{ + "HostInterfaceNetworking-vboxnet0": vboxNet, + } + + n := getHostOnlyAdapter(vboxNets, ip, ipnet.Mask) + if n != nil { + t.Fatalf("Expected vbox net to be nil but it has a value: %+v\n", n) + } +} + +// Tests a special case where Virtualbox creates the host only network +// successfully but mis-reports the netmask. +func TestGetHostOnlyNetworkWindows10Bug(t *testing.T) { + cidr := "192.168.99.0/24" + ip, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + t.Fatalf("Error parsing cidr: %s", err) + } + + // This is a faulty netmask: a VirtualBox bug causes it to be + // misreported. + ipnet.Mask = net.IPMask(net.ParseIP("15.0.0.0").To4()) + + expectedHostOnlyNetwork := &hostOnlyNetwork{ + IPv4: *ipnet, + } + + vboxNets := map[string]*hostOnlyNetwork{ + "HostInterfaceNetworking-vboxnet0": expectedHostOnlyNetwork, + } + + // The Mask that we are passing in will be the "legitimate" mask, so it + // must differ from the magic buggy mask. + n := getHostOnlyAdapter(vboxNets, ip, net.IPMask(net.ParseIP("255.255.255.0").To4())) + if !reflect.DeepEqual(n, expectedHostOnlyNetwork) { + t.Fatalf("Expected result of calling getHostOnlyNetwork to be the same as expected but it was not:\nexpected: %+v\nactual: %+v\n", expectedHostOnlyNetwork, n) + } +} + +func TestListHostOnlyNetworks(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "list hostonlyifs", + stdOut: stdOutOneHostOnlyNetwork, + } + + nets, err := listHostOnlyAdapters(vbox) + + assert.Equal(t, 1, len(nets)) + assert.NoError(t, err) + + net, present := nets["HostInterfaceNetworking-vboxnet0"] + + assert.True(t, present) + assert.Equal(t, "vboxnet0", net.Name) + assert.Equal(t, "786f6276-656e-4074-8000-0a0027000000", net.GUID) + assert.False(t, net.DHCP) + assert.Equal(t, "192.168.99.1", net.IPv4.IP.String()) + assert.Equal(t, "ffffff00", net.IPv4.Mask.String()) + assert.Equal(t, "0a:00:27:00:00:00", net.HwAddr.String()) + assert.Equal(t, "Ethernet", net.Medium) + assert.Equal(t, "Up", net.Status) + assert.Equal(t, "HostInterfaceNetworking-vboxnet0", net.NetworkName) +} + +func TestListTwoHostOnlyNetworks(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "list hostonlyifs", + stdOut: stdOutTwoHostOnlyNetwork, + } + + nets, err := listHostOnlyAdapters(vbox) + + assert.Equal(t, 2, len(nets)) + assert.NoError(t, err) + + net, present := nets["HostInterfaceNetworking-vboxnet1"] + + assert.True(t, present) + assert.Equal(t, "vboxnet1", net.Name) + assert.Equal(t, "786f6276-656e-4174-8000-0a0027000001", net.GUID) + assert.False(t, net.DHCP) + assert.Equal(t, "169.254.37.187", net.IPv4.IP.String()) + assert.Equal(t, "ffffff00", net.IPv4.Mask.String()) + assert.Equal(t, "0a:00:27:00:00:01", net.HwAddr.String()) + assert.Equal(t, "Ethernet", net.Medium) + assert.Equal(t, "Up", net.Status) + assert.Equal(t, "HostInterfaceNetworking-vboxnet1", net.NetworkName) +} + +func TestListHostOnlyNetworksDontRelyOnEmptyLinesForParsing(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "list hostonlyifs", + stdOut: `Name: vboxnet0 +VBoxNetworkName: HostInterfaceNetworking-vboxnet0 +Name: vboxnet1 +VBoxNetworkName: HostInterfaceNetworking-vboxnet1`, + } + + nets, err := listHostOnlyAdapters(vbox) + + assert.Equal(t, 2, len(nets)) + assert.NoError(t, err) + + net, present := nets["HostInterfaceNetworking-vboxnet1"] + assert.True(t, present) + assert.Equal(t, "vboxnet1", net.Name) + + net, present = nets["HostInterfaceNetworking-vboxnet0"] + assert.True(t, present) + assert.Equal(t, "vboxnet0", net.Name) +} + +func TestGetHostOnlyNetwork(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "list hostonlyifs", + stdOut: stdOutOneHostOnlyNetwork, + } + nets, err := listHostOnlyAdapters(vbox) + assert.NoError(t, err) + + net, err := getOrCreateHostOnlyNetwork(net.ParseIP("192.168.99.1"), parseIPv4Mask("255.255.255.0"), nets, vbox) + + assert.NotNil(t, net) + assert.Equal(t, "HostInterfaceNetworking-vboxnet0", net.NetworkName) + assert.NoError(t, err) +} + +func TestFailIfTwoNetworksHaveSameIP(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "list hostonlyifs", + stdOut: `Name: vboxnet0 +IPAddress: 192.168.99.1 +NetworkMask: 255.255.255.0 +VBoxNetworkName: HostInterfaceNetworking-vboxnet0 +Name: vboxnet1 +IPAddress: 192.168.99.1 +NetworkMask: 255.255.255.0 +VBoxNetworkName: HostInterfaceNetworking-vboxnet1`, + } + nets, err := listHostOnlyAdapters(vbox) + assert.Nil(t, nets) + assert.EqualError(t, err, `VirtualBox is configured with multiple host-only adapters with the same IP "192.168.99.1". Please remove one`) +} + +func TestFailIfTwoNetworksHaveSameName(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "list hostonlyifs", + stdOut: `Name: vboxnet0 +VBoxNetworkName: HostInterfaceNetworking-vboxnet0 +Name: vboxnet0 +VBoxNetworkName: HostInterfaceNetworking-vboxnet0`, + } + nets, err := listHostOnlyAdapters(vbox) + assert.Nil(t, nets) + assert.EqualError(t, err, `VirtualBox is configured with multiple host-only adapters with the same name "HostInterfaceNetworking-vboxnet0". Please remove one`) +} + +func TestGetDHCPServers(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "list dhcpservers", + stdOut: stdOutListTwoDHCPServers, + } + + servers, err := listDHCPServers(vbox) + + assert.Equal(t, 2, len(servers)) + assert.NoError(t, err) + + server, present := servers["HostInterfaceNetworking-vboxnet0"] + assert.True(t, present) + assert.Equal(t, "HostInterfaceNetworking-vboxnet0", server.NetworkName) + assert.Equal(t, "192.168.99.6", server.IPv4.IP.String()) + assert.Equal(t, "192.168.99.100", server.LowerIP.String()) + assert.Equal(t, "192.168.99.254", server.UpperIP.String()) + assert.Equal(t, "ffffff00", server.IPv4.Mask.String()) + assert.True(t, server.Enabled) + + server, present = servers["HostInterfaceNetworking-vboxnet1"] + assert.True(t, present) + assert.Equal(t, "HostInterfaceNetworking-vboxnet1", server.NetworkName) + assert.Equal(t, "192.168.99.7", server.IPv4.IP.String()) + assert.Equal(t, "192.168.99.100", server.LowerIP.String()) + assert.Equal(t, "192.168.99.254", server.UpperIP.String()) + assert.Equal(t, "ffffff00", server.IPv4.Mask.String()) + assert.False(t, server.Enabled) +} + +// Tests detection of a conflict between prospective vbox host-only network and an IPV6 host interface +func TestCheckIPNetCollisionIPv6(t *testing.T) { + m := map[string]*net.IPNet{} + _, vboxHostOnly, err := net.ParseCIDR("2607:f8b0:400e:c04:ffff:ffff:ffff:ffff/64") + assert.Nil(t, err) + + hostIP, hostNet, err := net.ParseCIDR("2001:4998:c:a06::2:4008/64") + assert.Nil(t, err) + m[hostIP.String()] = &net.IPNet{IP: hostIP, Mask: hostNet.Mask} + + result := checkIPNetCollision(vboxHostOnly, m) + assert.False(t, result) + + hostIP, hostNet, err = net.ParseCIDR("2607:f8b0:400e:c04::6a/64") + assert.Nil(t, err) + m[hostIP.String()] = &net.IPNet{IP: hostIP, Mask: hostNet.Mask} + + result = checkIPNetCollision(vboxHostOnly, m) + assert.True(t, result) +} + +// Tests detection of a conflict between prospective vbox host-only network and an IPV4 host interface +func TestCheckIPNetCollisionIPv4(t *testing.T) { + m := map[string]*net.IPNet{} + _, vboxHostOnly, err := net.ParseCIDR("192.168.99.1/24") + assert.NoError(t, err) + + hostIP, hostNet, err := net.ParseCIDR("10.10.10.42/24") + assert.NoError(t, err) + m[hostIP.String()] = &net.IPNet{IP: hostIP, Mask: hostNet.Mask} + + result := checkIPNetCollision(vboxHostOnly, m) + assert.False(t, result) + + hostIP, hostNet, err = net.ParseCIDR("192.168.99.22/24") + assert.NoError(t, err) + m[hostIP.String()] = &net.IPNet{IP: hostIP, Mask: hostNet.Mask} + + result = checkIPNetCollision(vboxHostOnly, m) + assert.True(t, result) +} + +// Tests functionality of listHostInterfaces and verifies only non-loopback, active and non-excluded interfaces are returned +func TestListHostInterfaces(t *testing.T) { + mhi := newMockHostInterfaces() + excludes := map[string]*hostOnlyNetwork{} + + en0, err := mhi.addMockIface("10.10.0.22", 24, net.IPv4len, "en0", net.FlagUp|net.FlagBroadcast) + assert.NoError(t, err) + _, err = mhi.addMockIface("10.10.1.11", 24, net.IPv4len, "en1", net.FlagBroadcast /*not up*/) + assert.NoError(t, err) + _, err = mhi.addMockIface("127.0.0.1", 24, net.IPv4len, "lo0", net.FlagUp|net.FlagLoopback) + assert.NoError(t, err) + en0ipv6, err := mhi.addMockIface("2001:4998:c:a06::2:4008", 64, net.IPv6len, "en0ipv6", net.FlagUp|net.FlagBroadcast) + assert.NoError(t, err) + vboxnet0, err := mhi.addMockIface("192.168.99.1", 24, net.IPv4len, "vboxnet0", net.FlagUp|net.FlagBroadcast) + assert.NoError(t, err) + notvboxnet0, err := mhi.addMockIface("192.168.99.42", 24, net.IPv4len, "en2", net.FlagUp|net.FlagBroadcast) + assert.NoError(t, err) + + excludes["192.168.99.1/24"] = &hostOnlyNetwork{IPv4: *vboxnet0, Name: "HostInterfaceNetworking-vboxnet0"} + + m, err := listHostInterfaces(mhi, excludes) + assert.NoError(t, err) + assert.NotEmpty(t, m) + + assert.Contains(t, m, "10.10.0.22/24") + assert.Equal(t, en0, m["10.10.0.22/24"]) + + assert.Contains(t, m, "2001:4998:c:a06::2:4008/64") + assert.Equal(t, en0ipv6, m["2001:4998:c:a06::2:4008/64"]) + + assert.Contains(t, m, "192.168.99.42/24") + assert.Equal(t, notvboxnet0, m["192.168.99.42/24"]) + + assert.NotContains(t, m, "10.10.1.11/24") + assert.NotContains(t, m, "127.0.0.1/24") + assert.NotContains(t, m, "192.168.99.1/24") +} diff --git a/pkg/drivers/virtualbox/vbm.go b/pkg/drivers/virtualbox/vbm.go new file mode 100644 index 000000000000..23384a4b6660 --- /dev/null +++ b/pkg/drivers/virtualbox/vbm.go @@ -0,0 +1,165 @@ +package virtualbox + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "os/exec" + "regexp" + "strings" + + "strconv" + + "time" + + "github.com/docker/machine/libmachine/log" +) + +const ( + retryCountOnObjectNotReadyError = 5 + objectNotReady = "error: The object is not ready" + retryDelay = 100 * time.Millisecond +) + +var ( + reColonLine = regexp.MustCompile(`(.+):\s+(.*)`) + reEqualLine = regexp.MustCompile(`(.+)=(.*)`) + reEqualQuoteLine = regexp.MustCompile(`"(.+)"="(.*)"`) + reMachineNotFound = regexp.MustCompile(`Could not find a registered machine named '(.+)'`) + + ErrMachineNotExist = errors.New("machine does not exist") + ErrVBMNotFound = errors.New("VBoxManage not found. Make sure VirtualBox is installed and VBoxManage is in the path") + + vboxManageCmd = detectVBoxManageCmd() +) + +// 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) +} + +// VBoxCmdManager communicates with VirtualBox through the commandline using `VBoxManage`. +type VBoxCmdManager struct { + runCmd func(cmd *exec.Cmd) error +} + +// NewVBoxManager creates a VBoxManager instance. +func NewVBoxManager() *VBoxCmdManager { + return &VBoxCmdManager{ + runCmd: func(cmd *exec.Cmd) error { return cmd.Run() }, + } +} + +func (v *VBoxCmdManager) vbm(args ...string) error { + _, _, err := v.vbmOutErr(args...) + return err +} + +func (v *VBoxCmdManager) vbmOut(args ...string) (string, error) { + stdout, _, err := v.vbmOutErr(args...) + return stdout, err +} + +func (v *VBoxCmdManager) vbmOutErr(args ...string) (string, string, error) { + return v.vbmOutErrRetry(retryCountOnObjectNotReadyError, args...) +} + +func (v *VBoxCmdManager) vbmOutErrRetry(retry int, args ...string) (string, string, error) { + cmd := exec.Command(vboxManageCmd, args...) + log.Debugf("COMMAND: %v %v", vboxManageCmd, strings.Join(args, " ")) + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := v.runCmd(cmd) + stderrStr := stderr.String() + if len(args) > 0 { + log.Debugf("STDOUT:\n{\n%v}", stdout.String()) + log.Debugf("STDERR:\n{\n%v}", stderrStr) + } + + if err != nil { + if ee, ok := err.(*exec.Error); ok && ee.Err == exec.ErrNotFound { + err = ErrVBMNotFound + } + } + + // Sometimes, we just need to retry... + if retry > 1 { + if strings.Contains(stderrStr, objectNotReady) { + time.Sleep(retryDelay) + return v.vbmOutErrRetry(retry-1, args...) + } + } + + if err == nil || strings.HasPrefix(err.Error(), "exit status ") { + // VBoxManage will sometimes not set the return code, but has a fatal error + // such as VBoxManage.exe: error: VT-x is not available. (VERR_VMX_NO_VMX) + if strings.Contains(stderrStr, "error:") { + err = fmt.Errorf("%v %v failed:\n%v", vboxManageCmd, strings.Join(args, " "), stderrStr) + } + } + + return stdout.String(), stderrStr, err +} + +func checkVBoxManageVersion(version string) error { + major, minor, err := parseVersion(version) + if (err != nil) || (major < 4) || (major == 4 && minor <= 2) { + return fmt.Errorf("We support Virtualbox starting with version 5. Your VirtualBox install is %q. Please upgrade at https://www.virtualbox.org", version) + } + + if major < 5 { + log.Warnf("You are using version %s of VirtualBox. If you encounter issues, you might want to upgrade to version 5 at https://www.virtualbox.org", version) + } + + return nil +} + +func parseVersion(version string) (int, int, error) { + parts := strings.Split(version, ".") + if len(parts) < 2 { + return 0, 0, fmt.Errorf("Invalid version: %q", version) + } + + major, err := strconv.Atoi(parts[0]) + if err != nil { + return 0, 0, fmt.Errorf("Invalid version: %q", version) + } + + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return 0, 0, fmt.Errorf("Invalid version: %q", version) + } + + return major, minor, err +} + +func parseKeyValues(stdOut string, re *regexp.Regexp, callback func(key, val string) error) error { + r := strings.NewReader(stdOut) + s := bufio.NewScanner(r) + + for s.Scan() { + line := s.Text() + if line == "" { + continue + } + + res := re.FindStringSubmatch(line) + if res == nil { + continue + } + + key, val := res[1], res[2] + if err := callback(key, val); err != nil { + return err + } + } + + return s.Err() +} diff --git a/pkg/drivers/virtualbox/vbm_test.go b/pkg/drivers/virtualbox/vbm_test.go new file mode 100644 index 000000000000..449c7881ba8a --- /dev/null +++ b/pkg/drivers/virtualbox/vbm_test.go @@ -0,0 +1,152 @@ +package virtualbox + +import ( + "testing" + + "os/exec" + + "errors" + + "fmt" + + "github.com/stretchr/testify/assert" +) + +func TestValidCheckVBoxManageVersion(t *testing.T) { + var tests = []struct { + version string + }{ + {"5.1"}, + {"5.0.8r103449"}, + {"5.0"}, + {"4.10"}, + {"4.3.1"}, + } + + for _, test := range tests { + err := checkVBoxManageVersion(test.version) + + assert.NoError(t, err) + } +} + +func TestInvalidCheckVBoxManageVersion(t *testing.T) { + var tests = []struct { + version string + expectedError string + }{ + {"3.9", `We support Virtualbox starting with version 5. Your VirtualBox install is "3.9". Please upgrade at https://www.virtualbox.org`}, + {"4.0", `We support Virtualbox starting with version 5. Your VirtualBox install is "4.0". Please upgrade at https://www.virtualbox.org`}, + {"4.1.1", `We support Virtualbox starting with version 5. Your VirtualBox install is "4.1.1". Please upgrade at https://www.virtualbox.org`}, + {"4.2.36-104064", `We support Virtualbox starting with version 5. Your VirtualBox install is "4.2.36-104064". Please upgrade at https://www.virtualbox.org`}, + {"X.Y", `We support Virtualbox starting with version 5. Your VirtualBox install is "X.Y". Please upgrade at https://www.virtualbox.org`}, + {"", `We support Virtualbox starting with version 5. Your VirtualBox install is "". Please upgrade at https://www.virtualbox.org`}, + } + + for _, test := range tests { + err := checkVBoxManageVersion(test.version) + + assert.EqualError(t, err, test.expectedError) + } +} + +func TestVbmOutErr(t *testing.T) { + var cmdRun *exec.Cmd + vBoxManager := NewVBoxManager() + vBoxManager.runCmd = func(cmd *exec.Cmd) error { + cmdRun = cmd + if _, err := fmt.Fprint(cmd.Stdout, "Printed to StdOut"); err != nil { + return err + } + if _, err := fmt.Fprint(cmd.Stderr, "Printed to StdErr"); err != nil { + return err + } + return nil + } + + stdOut, stdErr, err := vBoxManager.vbmOutErr("arg1", "arg2") + + assert.Equal(t, []string{vboxManageCmd, "arg1", "arg2"}, cmdRun.Args) + assert.Equal(t, "Printed to StdOut", stdOut) + assert.Equal(t, "Printed to StdErr", stdErr) + assert.NoError(t, err) +} + +func TestVbmOutErrError(t *testing.T) { + vBoxManager := NewVBoxManager() + vBoxManager.runCmd = func(cmd *exec.Cmd) error { return errors.New("BUG") } + + _, _, err := vBoxManager.vbmOutErr("arg1", "arg2") + + assert.EqualError(t, err, "BUG") +} + +func TestVbmOutErrNotFound(t *testing.T) { + vBoxManager := NewVBoxManager() + vBoxManager.runCmd = func(cmd *exec.Cmd) error { return &exec.Error{Err: exec.ErrNotFound} } + + _, _, err := vBoxManager.vbmOutErr("arg1", "arg2") + + assert.Equal(t, ErrVBMNotFound, err) +} + +func TestVbmOutErrFailingWithExitStatus(t *testing.T) { + vBoxManager := NewVBoxManager() + vBoxManager.runCmd = func(cmd *exec.Cmd) error { + if _, err := fmt.Fprint(cmd.Stderr, "error: Unable to run vbox"); err != nil { + return err + } + return errors.New("exit status BUG") + } + + _, _, err := vBoxManager.vbmOutErr("arg1", "arg2", "arg3") + + assert.EqualError(t, err, vboxManageCmd+" arg1 arg2 arg3 failed:\nerror: Unable to run vbox") +} + +func TestVbmOutErrRetryOnce(t *testing.T) { + var cmdRun *exec.Cmd + var runCount int + vBoxManager := NewVBoxManager() + vBoxManager.runCmd = func(cmd *exec.Cmd) error { + cmdRun = cmd + + runCount++ + if runCount == 1 { + _, err := fmt.Fprint(cmd.Stderr, "error: The object is not ready") + assert.NoError(t, err) + return errors.New("Fail the first time it's called") + } + + if _, err := fmt.Fprint(cmd.Stdout, "Printed to StdOut"); err != nil { + return err + } + return nil + } + + stdOut, stdErr, err := vBoxManager.vbmOutErr("command", "arg") + + assert.Equal(t, 2, runCount) + assert.Equal(t, []string{vboxManageCmd, "command", "arg"}, cmdRun.Args) + assert.Equal(t, "Printed to StdOut", stdOut) + assert.Empty(t, stdErr) + assert.NoError(t, err) +} + +func TestVbmOutErrRetryMax(t *testing.T) { + var runCount int + vBoxManager := NewVBoxManager() + vBoxManager.runCmd = func(cmd *exec.Cmd) error { + runCount++ + _, err := fmt.Fprint(cmd.Stderr, "error: The object is not ready") + assert.NoError(t, err) + return errors.New("Always fail") + } + + stdOut, stdErr, err := vBoxManager.vbmOutErr("command", "arg") + + assert.Equal(t, 5, runCount) + assert.Empty(t, stdOut) + assert.Equal(t, "error: The object is not ready", stdErr) + assert.Error(t, err) +} diff --git a/pkg/drivers/virtualbox/virtualbox.go b/pkg/drivers/virtualbox/virtualbox.go new file mode 100644 index 000000000000..24cf7eb9a3c0 --- /dev/null +++ b/pkg/drivers/virtualbox/virtualbox.go @@ -0,0 +1,1068 @@ +package virtualbox + +import ( + "errors" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "time" + + "github.com/docker/machine/libmachine/drivers" + "github.com/docker/machine/libmachine/log" + "github.com/docker/machine/libmachine/mcnflag" + "github.com/docker/machine/libmachine/mcnutils" + "github.com/docker/machine/libmachine/state" +) + +const ( + defaultCPU = 1 + defaultMemory = 1024 + defaultBoot2DockerURL = "" + defaultBoot2DockerImportVM = "" + defaultHostOnlyCIDR = "192.168.99.1/24" + defaultHostOnlyNictype = "82540EM" + defaultHostOnlyPromiscMode = "deny" + defaultUIType = "headless" + defaultHostOnlyNoDHCP = false + defaultDiskSize = 20000 + defaultDNSProxy = true + defaultDNSResolver = false + defaultHostLoopbackReachable = true +) + +var ( + ErrUnableToGenerateRandomIP = errors.New("unable to generate random IP") + ErrMustEnableVTX = errors.New("This computer doesn't have VT-X/AMD-v enabled. Enabling it in the BIOS is mandatory") + ErrNotCompatibleWithHyperV = errors.New("This computer is running Hyper-V. VirtualBox won't boot a 64bits VM when Hyper-V is activated. Either use Hyper-V as a driver, or disable the Hyper-V hypervisor. (To skip this check, use --virtualbox-no-vtx-check)") + ErrNetworkAddrCidr = errors.New("host-only cidr must be specified with a host address, not a network address") + ErrNetworkAddrCollision = errors.New("host-only cidr conflicts with the network address of a host interface") +) + +type Driver struct { + *drivers.BaseDriver + VBoxManager + HostInterfaces + b2dUpdater B2DUpdater + sshKeyGenerator SSHKeyGenerator + diskCreator DiskCreator + logsReader LogsReader + ipWaiter IPWaiter + randomInter RandomInter + sleeper Sleeper + version int + CPU int + Memory int + DiskSize int + NatNicType string + Boot2DockerURL string + Boot2DockerImportVM string + HostDNSResolver bool + HostLoopbackReachable bool + HostOnlyCIDR string + HostOnlyNicType string + HostOnlyPromiscMode string + UIType string + HostOnlyNoDHCP bool + NoShare bool + DNSProxy bool + NoVTXCheck bool + NoAccelerate3DOff bool + ShareFolder string +} + +// NewDriver creates a new VirtualBox driver with default settings. +func NewDriver(hostName, storePath string) *Driver { + return &Driver{ + VBoxManager: NewVBoxManager(), + b2dUpdater: NewB2DUpdater(), + sshKeyGenerator: NewSSHKeyGenerator(), + diskCreator: NewDiskCreator(), + logsReader: NewLogsReader(), + ipWaiter: NewIPWaiter(), + randomInter: NewRandomInter(), + sleeper: NewSleeper(), + HostInterfaces: NewHostInterfaces(), + Memory: defaultMemory, + CPU: defaultCPU, + DiskSize: defaultDiskSize, + NatNicType: defaultHostOnlyNictype, + HostOnlyCIDR: defaultHostOnlyCIDR, + HostOnlyNicType: defaultHostOnlyNictype, + HostOnlyPromiscMode: defaultHostOnlyPromiscMode, + UIType: defaultUIType, + HostOnlyNoDHCP: defaultHostOnlyNoDHCP, + DNSProxy: defaultDNSProxy, + HostDNSResolver: defaultDNSResolver, + HostLoopbackReachable: defaultHostLoopbackReachable, + BaseDriver: &drivers.BaseDriver{ + MachineName: hostName, + StorePath: storePath, + }, + } +} + +// GetCreateFlags registers the flags this driver adds to +// "docker hosts create" +func (d *Driver) GetCreateFlags() []mcnflag.Flag { + return []mcnflag.Flag{ + mcnflag.IntFlag{ + Name: "virtualbox-memory", + Usage: "Size of memory for host in MB", + Value: defaultMemory, + EnvVar: "VIRTUALBOX_MEMORY_SIZE", + }, + mcnflag.IntFlag{ + Name: "virtualbox-cpu-count", + Usage: "number of CPUs for the machine (-1 to use the number of CPUs available)", + Value: defaultCPU, + EnvVar: "VIRTUALBOX_CPU_COUNT", + }, + mcnflag.IntFlag{ + Name: "virtualbox-disk-size", + Usage: "Size of disk for host in MB", + Value: defaultDiskSize, + EnvVar: "VIRTUALBOX_DISK_SIZE", + }, + mcnflag.StringFlag{ + Name: "virtualbox-boot2docker-url", + Usage: "The URL of the boot2docker image. Defaults to the latest available version", + Value: defaultBoot2DockerURL, + EnvVar: "VIRTUALBOX_BOOT2DOCKER_URL", + }, + mcnflag.StringFlag{ + Name: "virtualbox-import-boot2docker-vm", + Usage: "The name of a Boot2Docker VM to import", + Value: defaultBoot2DockerImportVM, + EnvVar: "VIRTUALBOX_BOOT2DOCKER_IMPORT_VM", + }, + mcnflag.BoolFlag{ + Name: "virtualbox-host-dns-resolver", + Usage: "Use the host DNS resolver", + EnvVar: "VIRTUALBOX_HOST_DNS_RESOLVER", + }, + mcnflag.BoolFlag{ + Name: "virtualbox-host-loopback-reachable", + Usage: "Enable host loopback interface accessibility", + EnvVar: "VIRTUALBOX_HOST_LOOPBACK_REACHABLE", + }, + mcnflag.StringFlag{ + Name: "virtualbox-nat-nictype", + Usage: "Specify the Network Adapter Type", + Value: defaultHostOnlyNictype, + EnvVar: "VIRTUALBOX_NAT_NICTYPE", + }, + mcnflag.StringFlag{ + Name: "virtualbox-hostonly-cidr", + Usage: "Specify the Host Only CIDR", + Value: defaultHostOnlyCIDR, + EnvVar: "VIRTUALBOX_HOSTONLY_CIDR", + }, + mcnflag.StringFlag{ + Name: "virtualbox-hostonly-nictype", + Usage: "Specify the Host Only Network Adapter Type", + Value: defaultHostOnlyNictype, + EnvVar: "VIRTUALBOX_HOSTONLY_NIC_TYPE", + }, + mcnflag.StringFlag{ + Name: "virtualbox-hostonly-nicpromisc", + Usage: "Specify the Host Only Network Adapter Promiscuous Mode", + Value: defaultHostOnlyPromiscMode, + EnvVar: "VIRTUALBOX_HOSTONLY_NIC_PROMISC", + }, + mcnflag.StringFlag{ + Name: "virtualbox-ui-type", + Usage: "Specify the UI Type: (gui|sdl|headless|separate)", + Value: defaultUIType, + EnvVar: "VIRTUALBOX_UI_TYPE", + }, + mcnflag.BoolFlag{ + Name: "virtualbox-hostonly-no-dhcp", + Usage: "Disable the Host Only DHCP Server", + EnvVar: "VIRTUALBOX_HOSTONLY_NO_DHCP", + }, + mcnflag.BoolFlag{ + Name: "virtualbox-no-share", + Usage: "Disable the mount of your home directory", + EnvVar: "VIRTUALBOX_NO_SHARE", + }, + mcnflag.BoolFlag{ + Name: "virtualbox-no-dns-proxy", + Usage: "Disable proxying all DNS requests to the host", + EnvVar: "VIRTUALBOX_NO_DNS_PROXY", + }, + mcnflag.BoolFlag{ + Name: "virtualbox-no-vtx-check", + Usage: "Disable checking for the availability of hardware virtualization before the vm is started", + EnvVar: "VIRTUALBOX_NO_VTX_CHECK", + }, + mcnflag.BoolFlag{ + Name: "virtualbox-no-accelerate3d-off", + Usage: "Disable turning off the possibly missing 3D graphics acceleration before the vm is started", + EnvVar: "VIRTUALBOX_NO_ACCELERATE3D_OFF", + }, + mcnflag.StringFlag{ + EnvVar: "VIRTUALBOX_SHARE_FOLDER", + Name: "virtualbox-share-folder", + Usage: "Mount the specified directory instead of the default home location. Format: dir:name", + }, + } +} + +func (d *Driver) GetSSHHostname() (string, error) { + return "127.0.0.1", nil +} + +func (d *Driver) GetSSHUsername() string { + if d.SSHUser == "" { + d.SSHUser = "docker" + } + + return d.SSHUser +} + +// DriverName returns the name of the driver +func (d *Driver) DriverName() string { + return "virtualbox" +} + +func (d *Driver) GetURL() (string, error) { + ip, err := d.GetIP() + if err != nil { + return "", err + } + if ip == "" { + return "", nil + } + return fmt.Sprintf("tcp://%s:2376", ip), nil +} + +func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { + d.CPU = flags.Int("virtualbox-cpu-count") + d.Memory = flags.Int("virtualbox-memory") + d.DiskSize = flags.Int("virtualbox-disk-size") + d.Boot2DockerURL = flags.String("virtualbox-boot2docker-url") + d.SetSwarmConfigFromFlags(flags) + d.SSHUser = "docker" + d.Boot2DockerImportVM = flags.String("virtualbox-import-boot2docker-vm") + d.HostDNSResolver = flags.Bool("virtualbox-host-dns-resolver") + d.HostLoopbackReachable = flags.Bool("virtualbox-host-loopback-reachable") + d.NatNicType = flags.String("virtualbox-nat-nictype") + d.HostOnlyCIDR = flags.String("virtualbox-hostonly-cidr") + d.HostOnlyNicType = flags.String("virtualbox-hostonly-nictype") + d.HostOnlyPromiscMode = flags.String("virtualbox-hostonly-nicpromisc") + d.UIType = flags.String("virtualbox-ui-type") + d.HostOnlyNoDHCP = flags.Bool("virtualbox-hostonly-no-dhcp") + d.NoShare = flags.Bool("virtualbox-no-share") + d.DNSProxy = !flags.Bool("virtualbox-no-dns-proxy") + d.NoVTXCheck = flags.Bool("virtualbox-no-vtx-check") + d.NoAccelerate3DOff = flags.Bool("virtualbox-no-accelerate3d-off") + d.ShareFolder = flags.String("virtualbox-share-folder") + + return nil +} + +// PreCreateCheck checks that VBoxManage exists and works +func (d *Driver) PreCreateCheck() error { + // Check that VBoxManage exists and works + version, err := d.vbmOut("--version") + if err != nil { + return err + } + + // Check that VBoxManage is of a supported version + if err = checkVBoxManageVersion(strings.TrimSpace(version)); err != nil { + return err + } + + d.version, _, _ = parseVersion(strings.TrimSpace(version)) + + if !d.NoVTXCheck { + if isHyperVInstalled() { + return ErrNotCompatibleWithHyperV + } + + if d.IsVTXDisabled() { + return ErrMustEnableVTX + } + } + + // Downloading boot2docker to cache should be done here to make sure + // that a download failure will not leave a machine half created. + if err := d.b2dUpdater.UpdateISOCache(d.StorePath, d.Boot2DockerURL); err != nil { + return err + } + + // Check that Host-only interfaces are ok + if _, err = listHostOnlyAdapters(d.VBoxManager); err != nil { + return err + } + + return nil +} + +func (d *Driver) Create() error { + if err := d.CreateVM(); err != nil { + return err + } + + log.Info("Starting the VM...") + return d.Start() +} + +func importBoot2Docker(d *Driver, name string) error { + // make sure vm is stopped + _ = d.vbm("controlvm", name, "poweroff") + + diskInfo, err := getVMDiskInfo(name, d.VBoxManager) + if err != nil { + return err + } + + if _, err := os.Stat(diskInfo.Path); err != nil { + return err + } + + if err := d.vbm("clonehd", diskInfo.Path, d.diskPath()); err != nil { + return err + } + + log.Debugf("Importing VM settings...") + vmInfo, err := getVMInfo(name, d.VBoxManager) + if err != nil { + return err + } + + d.CPU = vmInfo.CPUs + d.Memory = vmInfo.Memory + + return nil +} + +func (d *Driver) CreateVM() error { + if err := d.b2dUpdater.CopyIsoToMachineDir(d.StorePath, d.MachineName, d.Boot2DockerURL); err != nil { + return err + } + + log.Info("Creating VirtualBox VM...") + + // import b2d VM if requested + if d.Boot2DockerImportVM != "" { + name := d.Boot2DockerImportVM + + if err := importBoot2Docker(d, name); err != nil { + return err + } + + log.Debugf("Importing SSH key...") + keyPath := filepath.Join(mcnutils.GetHomeDir(), ".ssh", "id_boot2docker") + if err := mcnutils.CopyFile(keyPath, d.GetSSHKeyPath()); err != nil { + return err + } + } else { + log.Infof("Creating SSH key...") + if err := d.sshKeyGenerator.Generate(d.GetSSHKeyPath()); err != nil { + return err + } + + log.Debugf("Creating disk image...") + if err := d.diskCreator.Create(d.DiskSize, d.publicSSHKeyPath(), d.diskPath()); err != nil { + return err + } + } + + if err := d.vbm("createvm", + "--basefolder", d.ResolveStorePath("."), + "--name", d.MachineName, + "--register"); err != nil { + return err + } + + log.Debugf("VM CPUS: %d", d.CPU) + log.Debugf("VM Memory: %d", d.Memory) + + cpus := d.CPU + if cpus < 1 { + cpus = runtime.NumCPU() + } + if cpus > 32 { + cpus = 32 + } + + hostDNSResolver := "off" + if d.HostDNSResolver { + hostDNSResolver = "on" + } + + hostLoopbackReachable := "off" + if d.HostLoopbackReachable { + hostLoopbackReachable = "on" + } + + dnsProxy := "off" + if d.DNSProxy { + dnsProxy = "on" + } + + var modifyFlags = []string{ + "modifyvm", d.MachineName, + "--firmware", "bios", + "--bioslogofadein", "off", + "--bioslogofadeout", "off", + "--bioslogodisplaytime", "0", + "--biosbootmenu", "disabled", + "--ostype", "Linux26_64", + "--cpus", fmt.Sprintf("%d", cpus), + "--memory", fmt.Sprintf("%d", d.Memory), + "--acpi", "on", + "--ioapic", "on", + "--rtcuseutc", "on", + "--natdnshostresolver1", hostDNSResolver, + "--natdnsproxy1", dnsProxy, + "--cpuhotplug", "off", + "--pae", "on", + "--hpet", "on", + "--hwvirtex", "on", + "--nestedpaging", "on", + "--largepages", "on", + "--vtxvpid", "on"} + if !d.NoAccelerate3DOff { + modifyFlags = append(modifyFlags, "--accelerate3d", "off") + } + modifyFlags = append(modifyFlags, "--boot1", "dvd") + + if d.version > 6 { + modifyFlags = append(modifyFlags, "--natlocalhostreachable1", hostLoopbackReachable) + } + + if runtime.GOOS == "windows" && runtime.GOARCH == "386" { + modifyFlags = append(modifyFlags, "--longmode", "on") + } + + if err := d.vbm(modifyFlags...); err != nil { + return err + } + + if err := d.vbm("modifyvm", d.MachineName, + "--nic1", "nat", + "--nictype1", d.NatNicType, + "--cableconnected1", "on"); err != nil { + return err + } + + if err := d.vbm("storagectl", d.MachineName, + "--name", "SATA", + "--add", "sata", + "--hostiocache", "on"); err != nil { + return err + } + + if err := d.vbm("storageattach", d.MachineName, + "--storagectl", "SATA", + "--port", "0", + "--device", "0", + "--type", "dvddrive", + "--medium", d.ResolveStorePath("boot2docker.iso")); err != nil { + return err + } + + if err := d.vbm("storageattach", d.MachineName, + "--storagectl", "SATA", + "--port", "1", + "--device", "0", + "--type", "hdd", + "--medium", d.diskPath()); err != nil { + return err + } + + // let VBoxService do nice magic automounting (when it's used) + if err := d.vbm("guestproperty", "set", d.MachineName, "/VirtualBox/GuestAdd/SharedFolders/MountPrefix", "/"); err != nil { + return err + } + if err := d.vbm("guestproperty", "set", d.MachineName, "/VirtualBox/GuestAdd/SharedFolders/MountDir", "/"); err != nil { + return err + } + + shareName, shareDir := getShareDriveAndName() + + if d.ShareFolder != "" { + shareDir, shareName = parseShareFolder(d.ShareFolder) + } + + if shareDir != "" && !d.NoShare { + if err := setupShareDir(d, shareDir, shareName); err != nil { + return err + } + } + + return nil +} + +func parseShareFolder(shareFolder string) (string, string) { + split := strings.Split(shareFolder, ":") + shareDir := strings.Join(split[:len(split)-1], ":") + shareName := split[len(split)-1] + return shareDir, shareName +} + +func setupShareDir(d *Driver, shareDir, shareName string) error { + log.Debugf("setting up shareDir '%s' -> '%s'", shareDir, shareName) + if _, err := os.Stat(shareDir); err != nil && !os.IsNotExist(err) { + return err + } else if !os.IsNotExist(err) { + 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 + } + + // woo, shareDir exists! let's carry on! + if err := d.vbm("sharedfolder", "add", d.MachineName, "--name", shareName, "--hostpath", shareDir, "--automount"); err != nil { + return err + } + + // enable symlinks + if err := d.vbm("setextradata", d.MachineName, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/"+shareName, "1"); err != nil { + return err + } + } + + return nil +} + +func (d *Driver) hostOnlyIPAvailable() bool { + ip, err := d.GetIP() + if err != nil { + log.Debugf("ERROR getting IP: %s", err) + return false + } + if ip == "" { + 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 { + s, err := d.GetState() + if err != nil { + return err + } + + var hostOnlyAdapter *hostOnlyNetwork + if s == state.Stopped { + log.Infof("Check network to re-create if needed...") + + if hostOnlyAdapter, err = d.setupHostOnlyNetwork(d.MachineName); err != nil { + return fmt.Errorf("Error setting up host only network on machine start: %s", err) + } + } + + switch s { + case state.Stopped, state.Saved: + d.SSHPort, err = setPortForwarding(d, 1, "ssh", "tcp", 22, d.SSHPort) + if err != nil { + return err + } + + if err := d.vbm("startvm", d.MachineName, "--type", d.UIType); err != nil { + if lines, readErr := d.readVBoxLog(); readErr == nil && len(lines) > 0 { + return fmt.Errorf("Unable to start the VM: %s\nDetails: %s", err, lines[len(lines)-1]) + } + return fmt.Errorf("Unable to start the VM: %s", err) + } + case state.Paused: + if err := d.vbm("controlvm", d.MachineName, "resume", "--type", d.UIType); err != nil { + return err + } + log.Infof("Resuming VM ...") + default: + log.Infof("VM not in restartable state") + } + + if !d.NoVTXCheck { + // Verify that VT-X is not disabled in the started VM + vtxIsDisabled, err := d.IsVTXDisabledInTheVM() + if err != nil { + return fmt.Errorf("Checking if hardware virtualization is enabled failed: %s", err) + } + + if vtxIsDisabled { + return ErrMustEnableVTX + } + } + + log.Infof("Waiting for an IP...") + if err := d.ipWaiter.Wait(d); err != nil { + return err + } + + if hostOnlyAdapter == nil { + return nil + } + + // Check that the host-only adapter we just created can still be found + // Sometimes it is corrupted after the VM is started. + nets, err := listHostOnlyAdapters(d.VBoxManager) + if err != nil { + return err + } + + ip, network, err := parseAndValidateCIDR(d.HostOnlyCIDR) + if err != nil { + return err + } + + err = validateNoIPCollisions(d.HostInterfaces, network, nets) + if err != nil { + return err + } + + hostOnlyNet := getHostOnlyAdapter(nets, ip, network.Mask) + if hostOnlyNet != nil { + // OK, we found a valid host-only adapter + return nil + } + + // This happens a lot on windows. The adapter has an invalid IP and the VM has the same IP + log.Warn("The host-only adapter is corrupted. Let's stop the VM, fix the host-only adapter and restart the VM") + if err := d.Stop(); err != nil { + return err + } + + // We have to be sure the host-only adapter is not used by the VM + d.sleeper.Sleep(5 * time.Second) + + log.Debugf("Fixing %+v...", hostOnlyAdapter) + if err := hostOnlyAdapter.SaveIPv4(d.VBoxManager); err != nil { + return err + } + + // We have to be sure the adapter is updated before starting the VM + d.sleeper.Sleep(5 * time.Second) + + if err := d.vbm("startvm", d.MachineName, "--type", d.UIType); err != nil { + return fmt.Errorf("Unable to start the VM: %s", err) + } + + log.Infof("Waiting for an IP...") + return d.ipWaiter.Wait(d) +} + +func (d *Driver) Stop() error { + currentState, err := d.GetState() + if err != nil { + return err + } + + if currentState == state.Paused { + if err := d.vbm("controlvm", d.MachineName, "resume"); err != nil { // , "--type", d.UIType + return err + } + log.Infof("Resuming VM ...") + } + + if err := d.vbm("controlvm", d.MachineName, "acpipowerbutton"); err != nil { + return err + } + for { + s, err := d.GetState() + if err != nil { + return err + } + if s == state.Running { + d.sleeper.Sleep(1 * time.Second) + } else { + break + } + } + + d.IPAddress = "" + + return nil +} + +// Restart restarts a machine which is known to be running. +func (d *Driver) Restart() error { + if err := d.Stop(); err != nil { + return fmt.Errorf("Problem stopping the VM: %s", err) + } + + if err := d.Start(); err != nil { + return fmt.Errorf("Problem starting the VM: %s", err) + } + + d.IPAddress = "" + + return d.ipWaiter.Wait(d) +} + +func (d *Driver) Kill() error { + return d.vbm("controlvm", d.MachineName, "poweroff") +} + +func (d *Driver) Remove() error { + s, err := d.GetState() + if err == ErrMachineNotExist { + return nil + } + if err != nil { + return err + } + + if s != state.Stopped && s != state.Saved { + if err := d.Kill(); err != nil { + return err + } + } + + return d.vbm("unregistervm", "--delete", d.MachineName) +} + +func (d *Driver) GetState() (state.State, error) { + stdout, stderr, err := d.vbmOutErr("showvminfo", d.MachineName, "--machinereadable") + if err != nil { + if reMachineNotFound.FindString(stderr) != "" { + return state.Error, ErrMachineNotExist + } + return state.Error, err + } + re := regexp.MustCompile(`(?m)^VMState="(\w+)"`) + groups := re.FindStringSubmatch(stdout) + if len(groups) < 1 { + return state.None, nil + } + switch groups[1] { + case "running": + return state.Running, nil + case "paused": + return state.Paused, nil + case "saved": + return state.Saved, nil + case "poweroff", "aborted": + return state.Stopped, nil + } + return state.None, nil +} + +func (d *Driver) getHostOnlyMACAddress() (string, error) { + // Return the MAC address of the host-only adapter + // assigned to this machine. The returned address + // is lower-cased and does not contain colons. + + stdout, stderr, err := d.vbmOutErr("showvminfo", d.MachineName, "--machinereadable") + if err != nil { + if reMachineNotFound.FindString(stderr) != "" { + return "", ErrMachineNotExist + } + return "", err + } + + // First, we get the number of the host-only interface + re := regexp.MustCompile(`(?m)^hostonlyadapter([\d]+)`) + groups := re.FindStringSubmatch(stdout) + if len(groups) < 2 { + return "", errors.New("Machine does not have a host-only adapter") + } + + // Then we grab the MAC address based on that number + adapterNumber := groups[1] + re = regexp.MustCompile(fmt.Sprintf("(?m)^macaddress%s=\"(.*)\"", adapterNumber)) + groups = re.FindStringSubmatch(stdout) + if len(groups) < 2 { + return "", fmt.Errorf("Could not find MAC address for adapter %v", adapterNumber) + } + + return strings.ToLower(groups[1]), nil +} + +func (d *Driver) parseIPForMACFromIPAddr(ipAddrOutput string, macAddress string) (string, error) { + // Given the output of "ip addr show" on the VM, return the IPv4 address + // of the interface with the given MAC address. + + lines := strings.Split(ipAddrOutput, "\n") + returnNextIP := false + + for _, line := range lines { + line = strings.TrimSpace(line) + + if strings.HasPrefix(line, "link") { // line contains MAC address + vals := strings.Split(line, " ") + if len(vals) >= 2 { + macBlock := vals[1] + macWithoutColons := strings.ReplaceAll(macBlock, ":", "") + if macWithoutColons == macAddress { // we are in the correct device block + returnNextIP = true + } + } + } else if strings.HasPrefix(line, "inet") && !strings.HasPrefix(line, "inet6") && returnNextIP { + vals := strings.Split(line, " ") + if len(vals) >= 2 { + idx := strings.Index(vals[1], "/") + if idx != -1 { + return vals[1][:idx], nil + } + return vals[1], nil + } + } + } + + return "", fmt.Errorf("Could not find matching IP for MAC address %v", macAddress) +} + +func (d *Driver) GetIP() (string, error) { + // DHCP is used to get the IP, so virtualbox hosts don't have IPs unless + // they are running + s, err := d.GetState() + if err != nil { + return "", err + } + if s != state.Running { + return "", drivers.ErrHostIsNotRunning + } + + macAddress, err := d.getHostOnlyMACAddress() + if err != nil { + return "", err + } + + log.Debugf("Host-only MAC: %s\n", macAddress) + + output, err := drivers.RunSSHCommandFromDriver(d, "ip addr show") + if err != nil { + return "", err + } + + log.Debugf("SSH returned: %s\nEND SSH\n", output) + + ipAddress, err := d.parseIPForMACFromIPAddr(output, macAddress) + if err != nil { + return "", err + } + + return ipAddress, nil +} + +func (d *Driver) publicSSHKeyPath() string { + return d.GetSSHKeyPath() + ".pub" +} + +func (d *Driver) diskPath() string { + return d.ResolveStorePath("disk.vmdk") +} + +func (d *Driver) setupHostOnlyNetwork(machineName string) (*hostOnlyNetwork, error) { + hostOnlyCIDR := d.HostOnlyCIDR + + // This is to assist in migrating from version 0.2 to 0.3 format + // it should be removed in a later release + if hostOnlyCIDR == "" { + hostOnlyCIDR = defaultHostOnlyCIDR + } + + ip, network, err := parseAndValidateCIDR(hostOnlyCIDR) + if err != nil { + return nil, err + } + + nets, err := listHostOnlyAdapters(d.VBoxManager) + if err != nil { + return nil, err + } + + err = validateNoIPCollisions(d.HostInterfaces, network, nets) + if err != nil { + return nil, err + } + + log.Debugf("Searching for hostonly interface for IPv4: %s and Mask: %s", ip, network.Mask) + hostOnlyAdapter, err := getOrCreateHostOnlyNetwork(ip, network.Mask, nets, d.VBoxManager) + if err != nil { + return nil, err + } + + if err := removeOrphanDHCPServers(d.VBoxManager); err != nil { + return nil, err + } + + dhcpAddr, err := getRandomIPinSubnet(d, ip) + if err != nil { + return nil, err + } + + lowerIP, upperIP := getDHCPAddressRange(dhcpAddr, network) + + log.Debugf("Adding/Modifying DHCP server %q with address range %q - %q...", dhcpAddr, lowerIP, upperIP) + + dhcp := dhcpServer{} + dhcp.IPv4.IP = dhcpAddr + dhcp.IPv4.Mask = network.Mask + dhcp.LowerIP = lowerIP + dhcp.UpperIP = upperIP + dhcp.Enabled = !d.HostOnlyNoDHCP + if err := addHostOnlyDHCPServer(hostOnlyAdapter.Name, dhcp, d.VBoxManager); err != nil { + return nil, err + } + + if err := d.vbm("modifyvm", machineName, + "--nic2", "hostonly", + "--nictype2", d.HostOnlyNicType, + "--nicpromisc2", d.HostOnlyPromiscMode, + "--hostonlyadapter2", hostOnlyAdapter.Name, + "--cableconnected2", "on"); err != nil { + return nil, err + } + + return hostOnlyAdapter, nil +} + +func getDHCPAddressRange(dhcpAddr net.IP, network *net.IPNet) (lowerIP net.IP, upperIP net.IP) { + nAddr := network.IP.To4() + ones, bits := network.Mask.Size() + + if ones <= 24 { + // For a /24 subnet, use the original behavior of allowing the address range + // between x.x.x.100 and x.x.x.254. + lowerIP = net.IPv4(nAddr[0], nAddr[1], nAddr[2], byte(100)) + upperIP = net.IPv4(nAddr[0], nAddr[1], nAddr[2], byte(254)) + return + } + + // Start the lowerIP range one address above the selected DHCP address. + lowerIP = net.IPv4(nAddr[0], nAddr[1], nAddr[2], dhcpAddr.To4()[3]+1) + + // The highest D-part of the address A.B.C.D in this subnet is at 2^n - 1, + // where n is the number of available bits in the subnet. Since the highest + // address is reserved for subnet broadcast, the highest *assignable* address + // is at (2^n - 1) - 1 == 2^n - 2. + maxAssignableSubnetAddress := (byte)((1 << (uint)(bits-ones)) - 2) + upperIP = net.IPv4(nAddr[0], nAddr[1], nAddr[2], maxAssignableSubnetAddress) + return +} + +func parseAndValidateCIDR(hostOnlyCIDR string) (net.IP, *net.IPNet, error) { + ip, network, err := net.ParseCIDR(hostOnlyCIDR) + if err != nil { + return nil, nil, err + } + + networkAddress := network.IP.To4() + if ip.Equal(networkAddress) { + return nil, nil, ErrNetworkAddrCidr + } + + return ip, network, nil +} + +// validateNoIPCollisions ensures no conflicts between the host's network interfaces and the vbox host-only network that +// will be used for machine vm instances. +func validateNoIPCollisions(hif HostInterfaces, hostOnlyNet *net.IPNet, currHostOnlyNets map[string]*hostOnlyNetwork) error { + hostOnlyByCIDR := map[string]*hostOnlyNetwork{} + // listHostOnlyAdapters returns a map w/ virtualbox net names as key. Rekey to CIDRs + for _, n := range currHostOnlyNets { + ipnet := net.IPNet{IP: n.IPv4.IP, Mask: n.IPv4.Mask} + hostOnlyByCIDR[ipnet.String()] = n + } + + m, err := listHostInterfaces(hif, hostOnlyByCIDR) + if err != nil { + return err + } + + collision := checkIPNetCollision(hostOnlyNet, m) + + if collision { + return ErrNetworkAddrCollision + } + return nil +} + +// Select an available port, trying the specified +// port first, falling back on an OS selected port. +func getAvailableTCPPort(port int) (int, error) { + for i := 0; i <= 10; i++ { + ln, err := net.Listen("tcp4", fmt.Sprintf("127.0.0.1:%d", port)) + if err != nil { + return 0, err + } + defer ln.Close() + addr := ln.Addr().String() + addrParts := strings.SplitN(addr, ":", 2) + p, err := strconv.Atoi(addrParts[1]) + if err != nil { + return 0, err + } + if p != 0 { + port = p + return port, nil + } + port = 0 // Throw away the port hint before trying again + time.Sleep(1 * time.Second) + } + return 0, fmt.Errorf("unable to allocate tcp port") +} + +// Setup a NAT port forwarding entry. +func setPortForwarding(d *Driver, interfaceNum int, mapName, protocol string, guestPort, desiredHostPort int) (int, error) { + actualHostPort, err := getAvailableTCPPort(desiredHostPort) + if err != nil { + return -1, err + } + if desiredHostPort != actualHostPort && desiredHostPort != 0 { + log.Debugf("NAT forwarding host port for guest port %d (%s) changed from %d to %d", + guestPort, mapName, desiredHostPort, actualHostPort) + } + cmd := fmt.Sprintf("--natpf%d", interfaceNum) + _ = 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 + } + return actualHostPort, nil +} + +// getRandomIPinSubnet returns a pseudo-random net.IP in the same +// subnet as the IP passed +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 := d.randomInter.RandomInt(24) + 1 + if byte(n) != nAddr[3] { + dhcpAddr = net.IPv4(nAddr[0], nAddr[1], nAddr[2], byte(n)) + break + } + } + + if dhcpAddr == nil { + return nil, ErrUnableToGenerateRandomIP + } + + return dhcpAddr, nil +} + +func detectVBoxManageCmdInPath() string { + cmd := "VBoxManage" + if path, err := exec.LookPath(cmd); err == nil { + return path + } + return cmd +} + +func (d *Driver) readVBoxLog() ([]string, error) { + logPath := filepath.Join(d.ResolveStorePath(d.MachineName), "Logs", "VBox.log") + log.Debugf("Checking vm logs: %s", logPath) + + return d.logsReader.Read(logPath) +} diff --git a/pkg/drivers/virtualbox/virtualbox_darwin.go b/pkg/drivers/virtualbox/virtualbox_darwin.go new file mode 100644 index 000000000000..0c225bde6d8e --- /dev/null +++ b/pkg/drivers/virtualbox/virtualbox_darwin.go @@ -0,0 +1,13 @@ +package virtualbox + +func detectVBoxManageCmd() string { + return detectVBoxManageCmdInPath() +} + +func getShareDriveAndName() (string, string) { + return "Users", "/Users" +} + +func isHyperVInstalled() bool { + return false +} diff --git a/pkg/drivers/virtualbox/virtualbox_darwin_test.go b/pkg/drivers/virtualbox/virtualbox_darwin_test.go new file mode 100644 index 000000000000..c7d65f05b170 --- /dev/null +++ b/pkg/drivers/virtualbox/virtualbox_darwin_test.go @@ -0,0 +1,15 @@ +package virtualbox + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestShareName(t *testing.T) { + name, dir := getShareDriveAndName() + + assert.Equal(t, name, "Users") + assert.Equal(t, dir, "/Users") + +} diff --git a/pkg/drivers/virtualbox/virtualbox_freebsd.go b/pkg/drivers/virtualbox/virtualbox_freebsd.go new file mode 100644 index 000000000000..be1a658d6ed6 --- /dev/null +++ b/pkg/drivers/virtualbox/virtualbox_freebsd.go @@ -0,0 +1,20 @@ +package virtualbox + +import "path/filepath" + +func detectVBoxManageCmd() string { + return detectVBoxManageCmdInPath() +} + +func getShareDriveAndName() (string, string) { + path, err := filepath.EvalSymlinks("/home") + if err != nil { + path = "/home" + } + + return "hosthome", path +} + +func isHyperVInstalled() bool { + return false +} diff --git a/pkg/drivers/virtualbox/virtualbox_linux.go b/pkg/drivers/virtualbox/virtualbox_linux.go new file mode 100644 index 000000000000..f41429e2ec61 --- /dev/null +++ b/pkg/drivers/virtualbox/virtualbox_linux.go @@ -0,0 +1,13 @@ +package virtualbox + +func detectVBoxManageCmd() string { + return detectVBoxManageCmdInPath() +} + +func getShareDriveAndName() (string, string) { + return "hosthome", "/home" +} + +func isHyperVInstalled() bool { + return false +} diff --git a/pkg/drivers/virtualbox/virtualbox_linux_test.go b/pkg/drivers/virtualbox/virtualbox_linux_test.go new file mode 100644 index 000000000000..7536db52f768 --- /dev/null +++ b/pkg/drivers/virtualbox/virtualbox_linux_test.go @@ -0,0 +1,14 @@ +package virtualbox + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestShareName(t *testing.T) { + name, dir := getShareDriveAndName() + + assert.Equal(t, name, "hosthome") + assert.Equal(t, dir, "/home") +} diff --git a/pkg/drivers/virtualbox/virtualbox_openbsd.go b/pkg/drivers/virtualbox/virtualbox_openbsd.go new file mode 100644 index 000000000000..f41429e2ec61 --- /dev/null +++ b/pkg/drivers/virtualbox/virtualbox_openbsd.go @@ -0,0 +1,13 @@ +package virtualbox + +func detectVBoxManageCmd() string { + return detectVBoxManageCmdInPath() +} + +func getShareDriveAndName() (string, string) { + return "hosthome", "/home" +} + +func isHyperVInstalled() bool { + return false +} diff --git a/pkg/drivers/virtualbox/virtualbox_test.go b/pkg/drivers/virtualbox/virtualbox_test.go new file mode 100644 index 000000000000..24e89464ff4b --- /dev/null +++ b/pkg/drivers/virtualbox/virtualbox_test.go @@ -0,0 +1,758 @@ +package virtualbox + +import ( + "errors" + "fmt" + "net" + "reflect" + "runtime" + "strings" + "testing" + "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() *Driver { + return NewDriver("default", "") +} + +func TestDriverName(t *testing.T) { + driverName := newTestDriver().DriverName() + + assert.Equal(t, "virtualbox", driverName) +} + +func TestSSHHostname(t *testing.T) { + hostname, err := newTestDriver().GetSSHHostname() + + assert.Equal(t, "127.0.0.1", hostname) + assert.NoError(t, err) +} + +func TestDefaultSSHUsername(t *testing.T) { + username := newTestDriver().GetSSHUsername() + + assert.Equal(t, "docker", username) +} + +var parseShareFolderTestCases = []struct { + shareFolder string + expectedShareDir string + expectedShareName string +}{ + {"dir:name", "dir", "name"}, + {"C:\\dir:name", "C:\\dir", "name"}, + {"C:\\:name", "C:\\", "name"}, +} + +func TestParseShareFolder(t *testing.T) { + for _, parseShareFolderTestCase := range parseShareFolderTestCases { + shareDir, shareName := parseShareFolder(parseShareFolderTestCase.shareFolder) + + assert.Equal(t, shareDir, parseShareFolderTestCase.expectedShareDir) + assert.Equal(t, shareName, parseShareFolderTestCase.expectedShareName) + } +} + +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() + 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() + 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() + + // 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 TestGetHostOnlyMACAddress(t *testing.T) { + driver := newTestDriver() + driver.VBoxManager = &VBoxManagerMock{ + args: "showvminfo default --machinereadable", + stdOut: "unrelatedfield=whatever\nhostonlyadapter2=\"vboxnet1\"\nmacaddress2=\"004488AABBCC\"\n", + } + + result, err := driver.getHostOnlyMACAddress() + expected := "004488aabbcc" + assert.NoError(t, err) + assert.Equal(t, expected, result) +} + +func TestGetHostOnlyMACAddressWhenNoHostOnlyAdapter(t *testing.T) { + driver := newTestDriver() + driver.VBoxManager = &VBoxManagerMock{ + args: "showvminfo default --machinereadable", + stdOut: "unrelatedfield=whatever\n", + } + + result, err := driver.getHostOnlyMACAddress() + assert.Empty(t, result) + assert.Equal(t, err, errors.New("Machine does not have a host-only adapter")) +} + +func TestParseIPForMACFromIPAddr(t *testing.T) { + driver := newTestDriver() + + ipAddrOutput := "1: eth0:\n link/ether 00:44:88:aa:bb:cc\n inet 1.2.3.4/24\n2: eth1:\n link/ether 11:55:99:dd:ee:ff\n inet 5.6.7.8/24" + + result, err := driver.parseIPForMACFromIPAddr(ipAddrOutput, "004488aabbcc") + assert.NoError(t, err) + assert.Equal(t, result, "1.2.3.4") + + result, err = driver.parseIPForMACFromIPAddr(ipAddrOutput, "115599ddeeff") + assert.NoError(t, err) + assert.Equal(t, result, "5.6.7.8") + + result, err = driver.parseIPForMACFromIPAddr(ipAddrOutput, "000000000000") + assert.Empty(t, result) + assert.Equal(t, err, errors.New("Could not find matching IP for MAC address 000000000000")) +} + +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() + 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) +} + +// Tests detection of a conflict between an existing vbox host-only network and a host network interface. This +// scenario would happen if the docker-machine was created with the host on one network, and then the host gets +// moved to another network (e.g. different wifi routers) +func TestCIDRHostIFaceCollisionExisting(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "list hostonlyifs", + stdOut: stdOutTwoHostOnlyNetwork, + } + mhi := newMockHostInterfaces() + _, err := mhi.addMockIface("192.168.99.42", 24, net.IPv4len, "en0", net.FlagUp|net.FlagBroadcast) + assert.NoError(t, err) + + nets, err := listHostOnlyAdapters(vbox) + assert.NoError(t, err) + m, listErr := listHostInterfaces(mhi, nets) + assert.Nil(t, listErr) + assert.NotEmpty(t, m) + + _, network, cidrErr := net.ParseCIDR("192.168.99.1/24") + assert.Nil(t, cidrErr) + err = validateNoIPCollisions(mhi, network, nets) + assert.Equal(t, ErrNetworkAddrCollision, err) +} + +// Tests operation of validateNoIPCollisions when no conflicts exist. +func TestCIDRHostIFaceNoCollision(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "list hostonlyifs", + stdOut: stdOutTwoHostOnlyNetwork, + } + mhi := newMockHostInterfaces() + _, err := mhi.addMockIface("10.10.0.22", 24, net.IPv4len, "en0", net.FlagUp|net.FlagBroadcast) + assert.NoError(t, err) + + nets, err := listHostOnlyAdapters(vbox) + assert.NoError(t, err) + m, listErr := listHostInterfaces(mhi, nets) + assert.Nil(t, listErr) + assert.NotEmpty(t, m) + + _, network, cidrErr := net.ParseCIDR("192.168.99.1/24") + assert.Nil(t, cidrErr) + err = validateNoIPCollisions(mhi, network, nets) + assert.NoError(t, err) +} + +// Tests detection of a conflict between a potential vbox host-only network and a host network interface. +func TestCIDRHostIFaceCollision(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "list hostonlyifs", + stdOut: "", + } + mhi := newMockHostInterfaces() + _, err := mhi.addMockIface("192.168.99.42", 24, net.IPv4len, "en0", net.FlagUp|net.FlagBroadcast) + assert.NoError(t, err) + + nets, err := listHostOnlyAdapters(vbox) + assert.NoError(t, err) + m, listErr := listHostInterfaces(mhi, nets) + assert.Nil(t, listErr) + assert.NotEmpty(t, m) + + _, network, cidrErr := net.ParseCIDR("192.168.99.1/24") + assert.Nil(t, cidrErr) + err = validateNoIPCollisions(mhi, network, nets) + assert.Equal(t, ErrNetworkAddrCollision, err) +} + +// Tests the behavior of getDHCPAddressRange with a variety of subnets. +func TestGetDHCPAddressRange(t *testing.T) { + tests := []struct { + name string + dhcpAddrCIDR string + expectedLowerIP net.IP + expectedUpperIP net.IP + }{ + { + "Test /8 CIDR", + "10.0.0.14/8", + net.ParseIP("10.0.0.100"), + net.ParseIP("10.0.0.254"), + }, + { + "Test /24 CIDR", + "192.168.99.7/24", + net.ParseIP("192.168.99.100"), + net.ParseIP("192.168.99.254"), + }, + { + "Test /25 CIDR", + "100.121.20.19/25", + net.ParseIP("100.121.20.20"), + net.ParseIP("100.121.20.126"), + }, + { + "Test /28 CIDR", + "100.121.10.8/28", + net.ParseIP("100.121.10.9"), + net.ParseIP("100.121.10.14"), + }, + } + + getTestArgsFromCIDR := func(cidr string) (dhcpAddr net.IP, network *net.IPNet) { + var err error + dhcpAddr, network, err = net.ParseCIDR(cidr) + assert.NoError(t, err, "Invalid CIDR %s", cidr) + return + } + + for _, tt := range tests { + dhcpAddr, network := getTestArgsFromCIDR(tt.dhcpAddrCIDR) + t.Run(tt.name, func(t *testing.T) { + lowerIP, upperIP := getDHCPAddressRange(dhcpAddr, network) + if !reflect.DeepEqual(lowerIP, tt.expectedLowerIP) { + t.Errorf("getDHCPAddressRange() lowerIP = %v, want %v", lowerIP, tt.expectedLowerIP) + } + if !reflect.DeepEqual(upperIP, tt.expectedUpperIP) { + t.Errorf("getDHCPAddressRange() upperIP = %v, want %v", upperIP, tt.expectedUpperIP) + } + }) + } +} + +func TestSetConfigFromFlags(t *testing.T) { + driver := newTestDriver() + + 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 5 +} + +func (v *MockCreateOperations) Sleep(d time.Duration) { + _, err := v.doCall("Sleep " + fmt.Sprintf("%v", d)) + if err != nil { + v.test.Fatal(err) + } +} + +func (v *MockCreateOperations) Interfaces() ([]net.Interface, error) { + _, err := v.doCall("Interfaces") + return []net.Interface{}, err +} + +func (v *MockCreateOperations) Addrs(iface *net.Interface) ([]net.Addr, error) { + _, err := v.doCall("Addrs " + fmt.Sprintf("%v", iface)) + return []net.Addr{}, 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 mockCalls(t *testing.T, driver *Driver, expectedCalls []Call) { + mockOperations := &MockCreateOperations{ + test: t, + expectedCalls: expectedCalls, + } + + driver.Boot2DockerURL = "http://b2d.org" + driver.VBoxManager = mockOperations + driver.b2dUpdater = mockOperations + driver.sshKeyGenerator = mockOperations + driver.diskCreator = mockOperations + driver.logsReader = mockOperations + driver.ipWaiter = mockOperations + driver.randomInter = mockOperations + driver.sleeper = mockOperations + driver.HostInterfaces = mockOperations +} + +func TestCreateVM(t *testing.T) { + shareName, shareDir := getShareDriveAndName() + + modifyVMcommand := "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 on --cpuhotplug off --pae on --hpet on --hwvirtex on --nestedpaging on --largepages on --vtxvpid on --accelerate3d off --boot1 dvd" + if runtime.GOOS == "windows" && runtime.GOARCH == "386" { + modifyVMcommand += " --longmode on" + } + + driver := NewDriver("default", "path") + mockCalls(t, driver, []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}, + {modifyVMcommand, "", 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}, + }) + + err := driver.CreateVM() + + assert.NoError(t, err) +} + +func TestCreateVMWithSpecificNatNicType(t *testing.T) { + shareName, shareDir := getShareDriveAndName() + + modifyVMcommand := "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 on --cpuhotplug off --pae on --hpet on --hwvirtex on --nestedpaging on --largepages on --vtxvpid on --accelerate3d off --boot1 dvd" + if runtime.GOOS == "windows" && runtime.GOARCH == "386" { + modifyVMcommand += " --longmode on" + } + + driver := NewDriver("default", "path") + driver.NatNicType = "Am79C973" + mockCalls(t, driver, []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}, + {modifyVMcommand, "", nil}, + {"vbm modifyvm default --nic1 nat --nictype1 Am79C973 --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}, + }) + + err := driver.CreateVM() + + assert.NoError(t, err) +} + +func TestCreateVMWithoutAccelerate3D(t *testing.T) { + shareName, shareDir := getShareDriveAndName() + + modifyVMcommand := "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 on --cpuhotplug off --pae on --hpet on --hwvirtex on --nestedpaging on --largepages on --vtxvpid on --boot1 dvd" + if runtime.GOOS == "windows" && runtime.GOARCH == "386" { + modifyVMcommand += " --longmode on" + } + + driver := NewDriver("default", "path") + driver.NoAccelerate3DOff = true + mockCalls(t, driver, []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}, + {modifyVMcommand, "", 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}, + }) + + err := driver.CreateVM() + + assert.NoError(t, err) +} + +func TestStart(t *testing.T) { + driver := NewDriver("default", "path") + mockCalls(t, driver, []Call{ + {"vbm showvminfo default --machinereadable", `VMState="poweroff"`, nil}, + {"vbm list hostonlyifs", "", nil}, + {"Interfaces", "", nil}, + {"vbm hostonlyif create", "Interface 'VirtualBox Host-Only Ethernet Adapter' was successfully created", 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 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}, + {"Interfaces", "", nil}, + }) + + err := driver.Start() + + assert.NoError(t, err) +} + +func TestStartWithHostOnlyAdapterCreationBug(t *testing.T) { + driver := NewDriver("default", "path") + mockCalls(t, driver, []Call{ + {"vbm showvminfo default --machinereadable", `VMState="poweroff"`, nil}, + {"vbm list hostonlyifs", "", nil}, + {"Interfaces", "", 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 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}, + {"Interfaces", "", 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}, + }) + + err := driver.Start() + + assert.NoError(t, err) +} + +func TestRemoveStopped(t *testing.T) { + driver := NewDriver("default", "path") + mockCalls(t, driver, []Call{ + {"vbm showvminfo default --machinereadable", `VMState="poweroff"`, nil}, + {"vbm unregistervm --delete default", "", nil}, + }) + + err := driver.Remove() + + assert.NoError(t, err) +} + +func TestRemoveStarted(t *testing.T) { + driver := NewDriver("default", "path") + mockCalls(t, driver, []Call{ + {"vbm showvminfo default --machinereadable", `VMState="running"`, nil}, + {"vbm controlvm default poweroff", "", nil}, + {"vbm unregistervm --delete default", "", nil}, + }) + + err := driver.Remove() + + assert.NoError(t, err) +} + +func TestRemoveSaved(t *testing.T) { + driver := NewDriver("default", "path") + mockCalls(t, driver, []Call{ + {"vbm showvminfo default --machinereadable", `VMState="saved"`, nil}, + {"vbm unregistervm --delete default", "", nil}, + }) + + err := driver.Remove() + + assert.NoError(t, err) +} + +func TestRemovePaused(t *testing.T) { + driver := NewDriver("default", "path") + mockCalls(t, driver, []Call{ + {"vbm showvminfo default --machinereadable", `VMState="running"`, nil}, + {"vbm controlvm default poweroff", "", nil}, + {"vbm unregistervm --delete default", "", nil}, + }) + + err := driver.Remove() + + assert.NoError(t, err) +} diff --git a/pkg/drivers/virtualbox/virtualbox_windows.go b/pkg/drivers/virtualbox/virtualbox_windows.go new file mode 100644 index 000000000000..c0af9dcf79a3 --- /dev/null +++ b/pkg/drivers/virtualbox/virtualbox_windows.go @@ -0,0 +1,105 @@ +package virtualbox + +import ( + "strings" + + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/docker/machine/libmachine/log" + "golang.org/x/sys/windows/registry" +) + +// cmdOutput runs a shell command and returns its output. +func cmdOutput(name string, args ...string) (string, error) { + cmd := exec.Command(name, args...) + log.Debugf("COMMAND: %v %v", name, strings.Join(args, " ")) + + stdout, err := cmd.Output() + if err != nil { + return "", err + } + + log.Debugf("STDOUT:\n{\n%v}", string(stdout)) + + return string(stdout), nil +} + +func detectVBoxManageCmd() string { + cmd := "VBoxManage" + 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 in default installation path for VirtualBox version > 5 + if path, err := exec.LookPath(filepath.Join("C:\\Program Files\\Oracle\\VirtualBox", cmd)); err == nil { + return path + } + + // Look in windows registry + if p, err := findVBoxInstallDirInRegistry(); err == nil { + if path, err := exec.LookPath(filepath.Join(p, cmd)); err == nil { + return path + } + } + + return detectVBoxManageCmdInPath() //fallback to path +} + +func findVBoxInstallDirInRegistry() (string, error) { + registryKey, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Oracle\VirtualBox`, registry.QUERY_VALUE) + if err != nil { + errorMessage := fmt.Sprintf("Can't find VirtualBox registry entries, is VirtualBox really installed properly? %s", err) + log.Debugf(errorMessage) + return "", fmt.Errorf("%s", errorMessage) + } + + defer registryKey.Close() + + installDir, _, err := registryKey.GetStringValue("InstallDir") + if err != nil { + errorMessage := fmt.Sprintf("Can't find InstallDir registry key within VirtualBox registries entries, is VirtualBox really installed properly? %s", err) + log.Debugf(errorMessage) + return "", fmt.Errorf("%s", errorMessage) + } + + return installDir, nil +} + +func getShareDriveAndName() (string, string) { + return "c/Users", "\\\\?\\c:\\Users" +} + +func isHyperVInstalled() bool { + // check if hyper-v is installed + _, err := exec.LookPath("vmms.exe") + if err != nil { + errmsg := "Hyper-V is not installed." + log.Debugf(errmsg, err) + return false + } + + // check to see if a hypervisor is present. if hyper-v is installed and enabled, + // display an error explaining the incompatibility between virtualbox and hyper-v. + output, err := cmdOutput("wmic", "computersystem", "get", "hypervisorpresent") + + if err != nil { + errmsg := "Could not check to see if Hyper-V is running." + log.Debugf(errmsg, err) + return false + } + + enabled := strings.Contains(output, "TRUE") + return enabled + +} diff --git a/pkg/drivers/virtualbox/vm.go b/pkg/drivers/virtualbox/vm.go new file mode 100644 index 000000000000..cd95ada76727 --- /dev/null +++ b/pkg/drivers/virtualbox/vm.go @@ -0,0 +1,41 @@ +package virtualbox + +import "strconv" + +type VM struct { + CPUs int + Memory int +} + +func getVMInfo(name string, vbox VBoxManager) (*VM, error) { + out, err := vbox.vbmOut("showvminfo", name, "--machinereadable") + if err != nil { + return nil, err + } + + vm := &VM{} + + err = parseKeyValues(out, reEqualLine, func(key, val string) error { + switch key { + case "cpus": + v, err := strconv.Atoi(val) + if err != nil { + return err + } + vm.CPUs = v + case "memory": + v, err := strconv.Atoi(val) + if err != nil { + return err + } + vm.Memory = v + } + + return nil + }) + if err != nil { + return nil, err + } + + return vm, nil +} diff --git a/pkg/drivers/virtualbox/vm_test.go b/pkg/drivers/virtualbox/vm_test.go new file mode 100644 index 000000000000..a90ffbf1ddb1 --- /dev/null +++ b/pkg/drivers/virtualbox/vm_test.go @@ -0,0 +1,44 @@ +package virtualbox + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +var stdOutVMInfo = ` +storagecontrollerbootable0="on" +memory=1024 +cpus=2 +"SATA-0-0"="/home/ehazlett/.boot2docker/boot2docker.iso" +"SATA-IsEjected"="off" +"SATA-1-0"="/home/ehazlett/vm/test/disk.vmdk" +"SATA-ImageUUID-1-0"="12345-abcdefg" +"SATA-2-0"="none" +nic1="nat"` + +func TestVMInfo(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "showvminfo host --machinereadable", + stdOut: stdOutVMInfo, + } + + vm, err := getVMInfo("host", vbox) + + assert.Equal(t, 2, vm.CPUs) + assert.Equal(t, 1024, vm.Memory) + assert.NoError(t, err) +} + +func TestVMInfoError(t *testing.T) { + vbox := &VBoxManagerMock{ + args: "showvminfo host --machinereadable", + err: errors.New("BUG"), + } + + vm, err := getVMInfo("host", vbox) + + assert.Nil(t, vm) + assert.EqualError(t, err, "BUG") +} diff --git a/pkg/drivers/virtualbox/vtx.go b/pkg/drivers/virtualbox/vtx.go new file mode 100644 index 000000000000..7adb631fe965 --- /dev/null +++ b/pkg/drivers/virtualbox/vtx.go @@ -0,0 +1,28 @@ +package virtualbox + +import "strings" + +// IsVTXDisabledInTheVM checks if VT-X is disabled in the started vm. +func (d *Driver) IsVTXDisabledInTheVM() (bool, error) { + lines, err := d.readVBoxLog() + if err != nil { + return true, err + } + + for _, line := range lines { + if strings.Contains(line, "VT-x is disabled") && !strings.Contains(line, "Falling back to raw-mode: VT-x is disabled in the BIOS for all CPU modes") { + return true, nil + } + if strings.Contains(line, "the host CPU does NOT support HW virtualization") { + return true, nil + } + if strings.Contains(line, "VERR_VMX_UNABLE_TO_START_VM") { + return true, nil + } + if strings.Contains(line, "Power up failed") && strings.Contains(line, "VERR_VMX_NO_VMX") { + return true, nil + } + } + + return false, nil +} diff --git a/pkg/drivers/virtualbox/vtx_intel.go b/pkg/drivers/virtualbox/vtx_intel.go new file mode 100644 index 000000000000..c8f6bb9799f0 --- /dev/null +++ b/pkg/drivers/virtualbox/vtx_intel.go @@ -0,0 +1,14 @@ +//go:build 386 || amd64 + +package virtualbox + +import "github.com/aregm/cpuid" + +// IsVTXDisabled checks if VT-x is disabled in the CPU. +func (d *Driver) IsVTXDisabled() bool { + if cpuid.HasFeature(cpuid.VMX) || cpuid.HasExtraFeature(cpuid.SVM) { + return false + } + + return true +} diff --git a/pkg/drivers/virtualbox/vtx_other.go b/pkg/drivers/virtualbox/vtx_other.go new file mode 100644 index 000000000000..eebd00f42d63 --- /dev/null +++ b/pkg/drivers/virtualbox/vtx_other.go @@ -0,0 +1,8 @@ +//go:build !386 && !amd64 + +package virtualbox + +// IsVTXDisabled checks if VT-x is disabled in the CPU. +func (d *Driver) IsVTXDisabled() bool { + return true +} diff --git a/pkg/drivers/virtualbox/vtx_test.go b/pkg/drivers/virtualbox/vtx_test.go new file mode 100644 index 000000000000..da90b15ab5e7 --- /dev/null +++ b/pkg/drivers/virtualbox/vtx_test.go @@ -0,0 +1,72 @@ +package virtualbox + +import ( + "testing" + + "errors" + + "github.com/stretchr/testify/assert" +) + +type MockLogsReader struct { + content []string + err error +} + +func (r *MockLogsReader) Read(path string) ([]string, error) { + return r.content, r.err +} + +func TestIsVTXEnabledInTheVM(t *testing.T) { + driver := NewDriver("default", "path") + + var tests = []struct { + description string + content []string + err error + }{ + {"Empty log", []string{}, nil}, + {"Raw mode", []string{"Falling back to raw-mode: VT-x is disabled in the BIOS for all CPU modes"}, nil}, + {"Raw mode", []string{"HM: HMR3Init: Falling back to raw-mode: VT-x is not available"}, nil}, + } + + for _, test := range tests { + driver.logsReader = &MockLogsReader{ + content: test.content, + err: test.err, + } + + disabled, err := driver.IsVTXDisabledInTheVM() + + assert.False(t, disabled, test.description) + assert.Equal(t, test.err, err) + } +} + +func TestIsVTXDisabledInTheVM(t *testing.T) { + driver := NewDriver("default", "path") + + var tests = []struct { + description string + content []string + err error + }{ + {"VT-x Disabled", []string{"VT-x is disabled"}, nil}, + {"No HW virtualization", []string{"the host CPU does NOT support HW virtualization"}, nil}, + {"Unable to start VM", []string{"VERR_VMX_UNABLE_TO_START_VM"}, nil}, + {"Power up failed", []string{"00:00:00.318604 Power up failed (vrc=VERR_VMX_NO_VMX, rc=NS_ERROR_FAILURE (0X80004005))"}, nil}, + {"Unable to read log", nil, errors.New("Unable to read log")}, + } + + for _, test := range tests { + driver.logsReader = &MockLogsReader{ + content: test.content, + err: test.err, + } + + disabled, err := driver.IsVTXDisabledInTheVM() + + assert.True(t, disabled, test.description) + assert.Equal(t, test.err, err) + } +} diff --git a/pkg/minikube/registry/drvs/hyperv/hyperv.go b/pkg/minikube/registry/drvs/hyperv/hyperv.go index 6928242d3cc6..5312757f8751 100644 --- a/pkg/minikube/registry/drvs/hyperv/hyperv.go +++ b/pkg/minikube/registry/drvs/hyperv/hyperv.go @@ -25,10 +25,10 @@ import ( "strings" "time" - "github.com/docker/machine/drivers/hyperv" "github.com/docker/machine/libmachine/drivers" "github.com/pkg/errors" + "k8s.io/minikube/pkg/drivers/hyperv" "k8s.io/minikube/pkg/minikube/config" "k8s.io/minikube/pkg/minikube/download" "k8s.io/minikube/pkg/minikube/driver" diff --git a/pkg/minikube/registry/drvs/virtualbox/virtualbox.go b/pkg/minikube/registry/drvs/virtualbox/virtualbox.go index 31aedc577eca..ea1e52d635f8 100644 --- a/pkg/minikube/registry/drvs/virtualbox/virtualbox.go +++ b/pkg/minikube/registry/drvs/virtualbox/virtualbox.go @@ -24,10 +24,10 @@ import ( "strings" "time" - "github.com/docker/machine/drivers/virtualbox" "github.com/docker/machine/libmachine/drivers" "k8s.io/klog/v2" + "k8s.io/minikube/pkg/drivers/virtualbox" "k8s.io/minikube/pkg/minikube/config" "k8s.io/minikube/pkg/minikube/download" "k8s.io/minikube/pkg/minikube/driver" From da783b3a521cd01f3035ade48632c517e0f7a617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Sat, 22 Nov 2025 12:40:06 +0100 Subject: [PATCH 2/3] Run the minikube copyright boilerplate script Use the year of the minikube-machine import --- pkg/drivers/hyperv/hyperv.go | 16 ++++++++++++++++ pkg/drivers/hyperv/powershell.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/disk.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/disk_test.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/ip.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/misc.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/network.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/network_test.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/vbm.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/vbm_test.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/virtualbox.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/virtualbox_darwin.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/virtualbox_darwin_test.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/virtualbox_freebsd.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/virtualbox_linux.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/virtualbox_linux_test.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/virtualbox_openbsd.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/virtualbox_test.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/virtualbox_windows.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/vm.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/vm_test.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/vtx.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/vtx_intel.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/vtx_other.go | 16 ++++++++++++++++ pkg/drivers/virtualbox/vtx_test.go | 16 ++++++++++++++++ 25 files changed, 400 insertions(+) diff --git a/pkg/drivers/hyperv/hyperv.go b/pkg/drivers/hyperv/hyperv.go index ec248112979e..5c644c882a41 100644 --- a/pkg/drivers/hyperv/hyperv.go +++ b/pkg/drivers/hyperv/hyperv.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package hyperv import ( diff --git a/pkg/drivers/hyperv/powershell.go b/pkg/drivers/hyperv/powershell.go index b619109520b5..96a63c885a08 100644 --- a/pkg/drivers/hyperv/powershell.go +++ b/pkg/drivers/hyperv/powershell.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package hyperv import ( diff --git a/pkg/drivers/virtualbox/disk.go b/pkg/drivers/virtualbox/disk.go index 82f064f6f395..af6dd517bd92 100644 --- a/pkg/drivers/virtualbox/disk.go +++ b/pkg/drivers/virtualbox/disk.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import ( diff --git a/pkg/drivers/virtualbox/disk_test.go b/pkg/drivers/virtualbox/disk_test.go index b6c591956e66..c4ae38d4beb9 100644 --- a/pkg/drivers/virtualbox/disk_test.go +++ b/pkg/drivers/virtualbox/disk_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import ( diff --git a/pkg/drivers/virtualbox/ip.go b/pkg/drivers/virtualbox/ip.go index be22a4fb7a9e..5a5eef8b2788 100644 --- a/pkg/drivers/virtualbox/ip.go +++ b/pkg/drivers/virtualbox/ip.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import ( diff --git a/pkg/drivers/virtualbox/misc.go b/pkg/drivers/virtualbox/misc.go index 3972a95443f8..777ad3e9a8f9 100644 --- a/pkg/drivers/virtualbox/misc.go +++ b/pkg/drivers/virtualbox/misc.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import ( diff --git a/pkg/drivers/virtualbox/network.go b/pkg/drivers/virtualbox/network.go index 633bf574d002..09a16d78b594 100644 --- a/pkg/drivers/virtualbox/network.go +++ b/pkg/drivers/virtualbox/network.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import ( diff --git a/pkg/drivers/virtualbox/network_test.go b/pkg/drivers/virtualbox/network_test.go index f2569536ed69..db7b5cdbc07f 100644 --- a/pkg/drivers/virtualbox/network_test.go +++ b/pkg/drivers/virtualbox/network_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import ( diff --git a/pkg/drivers/virtualbox/vbm.go b/pkg/drivers/virtualbox/vbm.go index 23384a4b6660..6d22a9d3197b 100644 --- a/pkg/drivers/virtualbox/vbm.go +++ b/pkg/drivers/virtualbox/vbm.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import ( diff --git a/pkg/drivers/virtualbox/vbm_test.go b/pkg/drivers/virtualbox/vbm_test.go index 449c7881ba8a..1721f4df4c25 100644 --- a/pkg/drivers/virtualbox/vbm_test.go +++ b/pkg/drivers/virtualbox/vbm_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import ( diff --git a/pkg/drivers/virtualbox/virtualbox.go b/pkg/drivers/virtualbox/virtualbox.go index 24cf7eb9a3c0..190baccf8546 100644 --- a/pkg/drivers/virtualbox/virtualbox.go +++ b/pkg/drivers/virtualbox/virtualbox.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import ( diff --git a/pkg/drivers/virtualbox/virtualbox_darwin.go b/pkg/drivers/virtualbox/virtualbox_darwin.go index 0c225bde6d8e..e7f7fdbd1cc9 100644 --- a/pkg/drivers/virtualbox/virtualbox_darwin.go +++ b/pkg/drivers/virtualbox/virtualbox_darwin.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox func detectVBoxManageCmd() string { diff --git a/pkg/drivers/virtualbox/virtualbox_darwin_test.go b/pkg/drivers/virtualbox/virtualbox_darwin_test.go index c7d65f05b170..494f1f220e5b 100644 --- a/pkg/drivers/virtualbox/virtualbox_darwin_test.go +++ b/pkg/drivers/virtualbox/virtualbox_darwin_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import ( diff --git a/pkg/drivers/virtualbox/virtualbox_freebsd.go b/pkg/drivers/virtualbox/virtualbox_freebsd.go index be1a658d6ed6..8e80b5a45802 100644 --- a/pkg/drivers/virtualbox/virtualbox_freebsd.go +++ b/pkg/drivers/virtualbox/virtualbox_freebsd.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import "path/filepath" diff --git a/pkg/drivers/virtualbox/virtualbox_linux.go b/pkg/drivers/virtualbox/virtualbox_linux.go index f41429e2ec61..b3af6f983e6b 100644 --- a/pkg/drivers/virtualbox/virtualbox_linux.go +++ b/pkg/drivers/virtualbox/virtualbox_linux.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox func detectVBoxManageCmd() string { diff --git a/pkg/drivers/virtualbox/virtualbox_linux_test.go b/pkg/drivers/virtualbox/virtualbox_linux_test.go index 7536db52f768..2eec2401e092 100644 --- a/pkg/drivers/virtualbox/virtualbox_linux_test.go +++ b/pkg/drivers/virtualbox/virtualbox_linux_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import ( diff --git a/pkg/drivers/virtualbox/virtualbox_openbsd.go b/pkg/drivers/virtualbox/virtualbox_openbsd.go index f41429e2ec61..b3af6f983e6b 100644 --- a/pkg/drivers/virtualbox/virtualbox_openbsd.go +++ b/pkg/drivers/virtualbox/virtualbox_openbsd.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox func detectVBoxManageCmd() string { diff --git a/pkg/drivers/virtualbox/virtualbox_test.go b/pkg/drivers/virtualbox/virtualbox_test.go index 24e89464ff4b..2e623b118833 100644 --- a/pkg/drivers/virtualbox/virtualbox_test.go +++ b/pkg/drivers/virtualbox/virtualbox_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import ( diff --git a/pkg/drivers/virtualbox/virtualbox_windows.go b/pkg/drivers/virtualbox/virtualbox_windows.go index c0af9dcf79a3..a57b20e55457 100644 --- a/pkg/drivers/virtualbox/virtualbox_windows.go +++ b/pkg/drivers/virtualbox/virtualbox_windows.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import ( diff --git a/pkg/drivers/virtualbox/vm.go b/pkg/drivers/virtualbox/vm.go index cd95ada76727..592e8e4540ba 100644 --- a/pkg/drivers/virtualbox/vm.go +++ b/pkg/drivers/virtualbox/vm.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import "strconv" diff --git a/pkg/drivers/virtualbox/vm_test.go b/pkg/drivers/virtualbox/vm_test.go index a90ffbf1ddb1..341841fd54b8 100644 --- a/pkg/drivers/virtualbox/vm_test.go +++ b/pkg/drivers/virtualbox/vm_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import ( diff --git a/pkg/drivers/virtualbox/vtx.go b/pkg/drivers/virtualbox/vtx.go index 7adb631fe965..5801863685a2 100644 --- a/pkg/drivers/virtualbox/vtx.go +++ b/pkg/drivers/virtualbox/vtx.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import "strings" diff --git a/pkg/drivers/virtualbox/vtx_intel.go b/pkg/drivers/virtualbox/vtx_intel.go index c8f6bb9799f0..22184c5cacda 100644 --- a/pkg/drivers/virtualbox/vtx_intel.go +++ b/pkg/drivers/virtualbox/vtx_intel.go @@ -1,5 +1,21 @@ //go:build 386 || amd64 +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import "github.com/aregm/cpuid" diff --git a/pkg/drivers/virtualbox/vtx_other.go b/pkg/drivers/virtualbox/vtx_other.go index eebd00f42d63..88204be4e88b 100644 --- a/pkg/drivers/virtualbox/vtx_other.go +++ b/pkg/drivers/virtualbox/vtx_other.go @@ -1,5 +1,21 @@ //go:build !386 && !amd64 +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox // IsVTXDisabled checks if VT-x is disabled in the CPU. diff --git a/pkg/drivers/virtualbox/vtx_test.go b/pkg/drivers/virtualbox/vtx_test.go index da90b15ab5e7..c0d73c403639 100644 --- a/pkg/drivers/virtualbox/vtx_test.go +++ b/pkg/drivers/virtualbox/vtx_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +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. +*/ + package virtualbox import ( From f7c9a93757611cb83a7bfb680dda9add42d627cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Sat, 22 Nov 2025 13:03:12 +0100 Subject: [PATCH 3/3] Fix lint issues in the imported drivers Note: we can't change exported errors Since they are used in "known issues" --- pkg/drivers/hyperv/hyperv.go | 12 +++++++----- pkg/drivers/hyperv/powershell.go | 1 + pkg/drivers/virtualbox/disk.go | 1 + pkg/drivers/virtualbox/network.go | 13 +++++++++---- pkg/drivers/virtualbox/vbm.go | 7 ++++--- pkg/drivers/virtualbox/virtualbox.go | 13 ++++++++++++- pkg/drivers/virtualbox/vm.go | 1 + 7 files changed, 35 insertions(+), 13 deletions(-) diff --git a/pkg/drivers/hyperv/hyperv.go b/pkg/drivers/hyperv/hyperv.go index 5c644c882a41..5abc128cf2ce 100644 --- a/pkg/drivers/hyperv/hyperv.go +++ b/pkg/drivers/hyperv/hyperv.go @@ -25,6 +25,8 @@ import ( "strings" "time" + "github.com/pkg/errors" + "github.com/docker/machine/libmachine/drivers" "github.com/docker/machine/libmachine/log" "github.com/docker/machine/libmachine/mcnflag" @@ -159,7 +161,7 @@ func (d *Driver) GetURL() (string, error) { func (d *Driver) GetState() (state.State, error) { stdout, err := cmdOut("(", "Hyper-V\\Get-VM", d.MachineName, ").state") if err != nil { - return state.None, fmt.Errorf("Failed to find the VM status") + return state.None, errors.New("Failed to find the VM status") } resp := parseLines(stdout) @@ -320,11 +322,11 @@ func (d *Driver) chooseVirtualSwitch() (string, error) { // prefer Default Switch over external switches switches, err := getHyperVSwitches([]string{fmt.Sprintf("Where-Object {($_.SwitchType -eq 'External') -or ($_.Id -eq '%s')}", defaultSwitchID), "Sort-Object -Property SwitchType"}) if err != nil { - return "", fmt.Errorf("unable to get available hyperv switches") + return "", errors.New("unable to get available hyperv switches") } if len(switches) < 1 { - return "", fmt.Errorf("no External vswitch nor Default Switch found. A valid vswitch must be available for this command to run. Check https://docs.docker.com/machine/drivers/hyper-v/") + return "", errors.New("no External vswitch nor Default Switch found. A valid vswitch must be available for this command to run. Check https://docs.docker.com/machine/drivers/hyper-v/") } return switches[0].Name, nil @@ -333,7 +335,7 @@ func (d *Driver) chooseVirtualSwitch() (string, error) { // prefer external switches (using descending order) switches, err := getHyperVSwitches([]string{fmt.Sprintf("Where-Object {$_.Name -eq '%s'}", d.VSwitch), "Sort-Object -Property SwitchType -Descending"}) if err != nil { - return "", fmt.Errorf("unable to get available hyperv switches") + return "", errors.New("unable to get available hyperv switches") } if len(switches) < 1 { @@ -460,7 +462,7 @@ func (d *Driver) GetIP() (string, error) { resp := parseLines(stdout) if len(resp) < 1 { - return "", fmt.Errorf("IP not found") + return "", errors.New("IP not found") } return resp[0], nil diff --git a/pkg/drivers/hyperv/powershell.go b/pkg/drivers/hyperv/powershell.go index 96a63c885a08..2c928f876a7c 100644 --- a/pkg/drivers/hyperv/powershell.go +++ b/pkg/drivers/hyperv/powershell.go @@ -30,6 +30,7 @@ import ( var powershell string +//nolint:staticcheck // ST1005: error strings should not be capitalized var ( ErrPowerShellNotFound = errors.New("Powershell was not found in the path") ErrNotAdministrator = errors.New("Hyper-v commands have to be run as an Administrator") diff --git a/pkg/drivers/virtualbox/disk.go b/pkg/drivers/virtualbox/disk.go index af6dd517bd92..b2e5f8e3a53d 100644 --- a/pkg/drivers/virtualbox/disk.go +++ b/pkg/drivers/virtualbox/disk.go @@ -145,6 +145,7 @@ func getVMDiskInfo(name string, vbox VBoxManager) (*VirtualDisk, error) { disk.Path = val case "SATA-ImageUUID-1-0": disk.UUID = val + default: } return nil diff --git a/pkg/drivers/virtualbox/network.go b/pkg/drivers/virtualbox/network.go index 09a16d78b594..869a8f7f59d5 100644 --- a/pkg/drivers/virtualbox/network.go +++ b/pkg/drivers/virtualbox/network.go @@ -34,9 +34,12 @@ const ( dhcpPrefix = "HostInterfaceNetworking-" ) +//nolint:staticcheck // ST1005: error strings should not be capitalized var ( - reHostOnlyAdapterCreated = regexp.MustCompile(`Interface '(.+)' was successfully created`) - errNewHostOnlyAdapterNotVisible = errors.New("The host-only adapter we just created is not visible. This is a well known VirtualBox bug. You might want to uninstall it and reinstall at least version 5.0.12 that is is supposed to fix this issue") + reHostOnlyAdapterCreated = regexp.MustCompile(`Interface '(.+)' was successfully created`) + errNewHostOnlyAdapterNotVisible = errors.New("The host-only adapter we just created is not visible. This is a well known VirtualBox bug. You might want to uninstall it and reinstall at least version 5.0.12 that is is supposed to fix this issue") + errFailedToCreateHostOnlyAdapter = errors.New("Failed to create host-only adapter") + errFailedToFindHostOnlyAdapter = errors.New("Failed to find a new host-only adapter") ) // Host-only network. @@ -113,7 +116,7 @@ func createHostonlyAdapter(vbox VBoxManager) (*hostOnlyNetwork, error) { res := reHostOnlyAdapterCreated.FindStringSubmatch(out) if res == nil { - return nil, errors.New("Failed to create host-only adapter") + return nil, errFailedToCreateHostOnlyAdapter } return &hostOnlyNetwork{Name: res[1]}, nil @@ -168,6 +171,7 @@ func listHostOnlyAdapters(vbox VBoxManager) (map[string]*hostOnlyNetwork, error) } n = &hostOnlyNetwork{} + default: } return nil @@ -245,7 +249,7 @@ func waitForNewHostOnlyNetwork(oldNets map[string]*hostOnlyNetwork, vbox VBoxMan } } - return nil, errors.New("Failed to find a new host-only adapter") + return nil, errFailedToFindHostOnlyAdapter } // DHCP server info. @@ -354,6 +358,7 @@ func listDHCPServers(vbox VBoxManager) (map[string]*dhcpServer, error) { dhcp.IPv4.Mask = parseIPv4Mask(val) case "Enabled": dhcp.Enabled = (val == "Yes") + default: } return nil diff --git a/pkg/drivers/virtualbox/vbm.go b/pkg/drivers/virtualbox/vbm.go index 6d22a9d3197b..1fb7a85fdf84 100644 --- a/pkg/drivers/virtualbox/vbm.go +++ b/pkg/drivers/virtualbox/vbm.go @@ -127,6 +127,7 @@ func (v *VBoxCmdManager) vbmOutErrRetry(retry int, args ...string) (string, stri func checkVBoxManageVersion(version string) error { major, minor, err := parseVersion(version) if (err != nil) || (major < 4) || (major == 4 && minor <= 2) { + //nolint:staticcheck // ST1005: error strings should not be capitalized return fmt.Errorf("We support Virtualbox starting with version 5. Your VirtualBox install is %q. Please upgrade at https://www.virtualbox.org", version) } @@ -140,17 +141,17 @@ func checkVBoxManageVersion(version string) error { func parseVersion(version string) (int, int, error) { parts := strings.Split(version, ".") if len(parts) < 2 { - return 0, 0, fmt.Errorf("Invalid version: %q", version) + return 0, 0, fmt.Errorf("invalid version: %q", version) } major, err := strconv.Atoi(parts[0]) if err != nil { - return 0, 0, fmt.Errorf("Invalid version: %q", version) + return 0, 0, fmt.Errorf("invalid version: %q", version) } minor, err := strconv.Atoi(parts[1]) if err != nil { - return 0, 0, fmt.Errorf("Invalid version: %q", version) + return 0, 0, fmt.Errorf("invalid version: %q", version) } return major, minor, err diff --git a/pkg/drivers/virtualbox/virtualbox.go b/pkg/drivers/virtualbox/virtualbox.go index 190baccf8546..3246512d77fa 100644 --- a/pkg/drivers/virtualbox/virtualbox.go +++ b/pkg/drivers/virtualbox/virtualbox.go @@ -52,6 +52,7 @@ const ( defaultHostLoopbackReachable = true ) +//nolint:staticcheck // ST1005: error strings should not be capitalized var ( ErrUnableToGenerateRandomIP = errors.New("unable to generate random IP") ErrMustEnableVTX = errors.New("This computer doesn't have VT-X/AMD-v enabled. Enabling it in the BIOS is mandatory") @@ -578,6 +579,7 @@ func (d *Driver) Start() error { log.Infof("Check network to re-create if needed...") if hostOnlyAdapter, err = d.setupHostOnlyNetwork(d.MachineName); err != nil { + //nolint:staticcheck // ST1005: error strings should not be capitalized return fmt.Errorf("Error setting up host only network on machine start: %s", err) } } @@ -591,8 +593,10 @@ func (d *Driver) Start() error { if err := d.vbm("startvm", d.MachineName, "--type", d.UIType); err != nil { if lines, readErr := d.readVBoxLog(); readErr == nil && len(lines) > 0 { + //nolint:staticcheck // ST1005: error strings should not be capitalized return fmt.Errorf("Unable to start the VM: %s\nDetails: %s", err, lines[len(lines)-1]) } + //nolint:staticcheck // ST1005: error strings should not be capitalized return fmt.Errorf("Unable to start the VM: %s", err) } case state.Paused: @@ -608,6 +612,7 @@ func (d *Driver) Start() error { // Verify that VT-X is not disabled in the started VM vtxIsDisabled, err := d.IsVTXDisabledInTheVM() if err != nil { + //nolint:staticcheck // ST1005: error strings should not be capitalized return fmt.Errorf("Checking if hardware virtualization is enabled failed: %s", err) } @@ -666,6 +671,7 @@ func (d *Driver) Start() error { d.sleeper.Sleep(5 * time.Second) if err := d.vbm("startvm", d.MachineName, "--type", d.UIType); err != nil { + //nolint:staticcheck // ST1005: error strings should not be capitalized return fmt.Errorf("Unable to start the VM: %s", err) } @@ -709,10 +715,12 @@ func (d *Driver) Stop() error { // Restart restarts a machine which is known to be running. func (d *Driver) Restart() error { if err := d.Stop(); err != nil { + //nolint:staticcheck // ST1005: error strings should not be capitalized return fmt.Errorf("Problem stopping the VM: %s", err) } if err := d.Start(); err != nil { + //nolint:staticcheck // ST1005: error strings should not be capitalized return fmt.Errorf("Problem starting the VM: %s", err) } @@ -786,6 +794,7 @@ func (d *Driver) getHostOnlyMACAddress() (string, error) { re := regexp.MustCompile(`(?m)^hostonlyadapter([\d]+)`) groups := re.FindStringSubmatch(stdout) if len(groups) < 2 { + //nolint:staticcheck // ST1005: error strings should not be capitalized return "", errors.New("Machine does not have a host-only adapter") } @@ -794,6 +803,7 @@ func (d *Driver) getHostOnlyMACAddress() (string, error) { re = regexp.MustCompile(fmt.Sprintf("(?m)^macaddress%s=\"(.*)\"", adapterNumber)) groups = re.FindStringSubmatch(stdout) if len(groups) < 2 { + //nolint:staticcheck // ST1005: error strings should not be capitalized return "", fmt.Errorf("Could not find MAC address for adapter %v", adapterNumber) } @@ -831,6 +841,7 @@ func (d *Driver) parseIPForMACFromIPAddr(ipAddrOutput string, macAddress string) } } + //nolint:staticcheck // ST1005: error strings should not be capitalized return "", fmt.Errorf("Could not find matching IP for MAC address %v", macAddress) } @@ -1023,7 +1034,7 @@ func getAvailableTCPPort(port int) (int, error) { port = 0 // Throw away the port hint before trying again time.Sleep(1 * time.Second) } - return 0, fmt.Errorf("unable to allocate tcp port") + return 0, errors.New("unable to allocate tcp port") } // Setup a NAT port forwarding entry. diff --git a/pkg/drivers/virtualbox/vm.go b/pkg/drivers/virtualbox/vm.go index 592e8e4540ba..006a3354aa07 100644 --- a/pkg/drivers/virtualbox/vm.go +++ b/pkg/drivers/virtualbox/vm.go @@ -45,6 +45,7 @@ func getVMInfo(name string, vbox VBoxManager) (*VM, error) { return err } vm.Memory = v + default: } return nil