From 52ab263df78c1e77a4aea79b172a02eeb51c0286 Mon Sep 17 00:00:00 2001 From: Jimmi Dyson Date: Wed, 27 Jul 2016 17:15:48 +0100 Subject: [PATCH] Add hyperv support --- Godeps/Godeps.json | 5 + docs/minikube_start.md | 2 +- pkg/minikube/cluster/cluster.go | 2 + .../cluster/cluster_non_windows_panic.go | 25 + pkg/minikube/cluster/cluster_windows.go | 32 ++ pkg/minikube/constants/constants_gendocs.go | 1 + pkg/minikube/constants/constants_windows.go | 1 + pkg/minikube/machine/client.go | 3 + .../docker/machine/drivers/hyperv/hyperv.go | 476 ++++++++++++++++++ .../machine/drivers/hyperv/powershell.go | 114 +++++ 10 files changed, 660 insertions(+), 1 deletion(-) create mode 100644 pkg/minikube/cluster/cluster_non_windows_panic.go create mode 100644 pkg/minikube/cluster/cluster_windows.go create mode 100644 vendor/github.com/docker/machine/drivers/hyperv/hyperv.go create mode 100644 vendor/github.com/docker/machine/drivers/hyperv/powershell.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 0abdd7c5c75c..ca1d6faaa183 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -705,6 +705,11 @@ "Comment": "v0.7.0-62-g6002b41", "Rev": "6002b411ce820eaf03ac972a7fb354bb56f7aa95" }, + { + "ImportPath": "github.com/docker/machine/drivers/hyperv", + "Comment": "v0.7.0-62-g6002b41", + "Rev": "6002b411ce820eaf03ac972a7fb354bb56f7aa95" + }, { "ImportPath": "github.com/docker/machine/drivers/none", "Comment": "v0.7.0-62-g6002b41", diff --git a/docs/minikube_start.md b/docs/minikube_start.md index 5554787680a0..691270a7084c 100644 --- a/docs/minikube_start.md +++ b/docs/minikube_start.md @@ -24,7 +24,7 @@ minikube start --kubernetes-version="v1.3.4": The kubernetes version that the minikube VM will (ex: v1.2.3) OR a URI which contains a localkube binary (ex: https://storage.googleapis.com/minikube/k8sReleases/v1.3.0/localkube-linux-amd64) --memory=1024: Amount of RAM allocated to the minikube VM - --vm-driver="virtualbox": VM driver is one of: [virtualbox vmwarefusion kvm xhyve] + --vm-driver="virtualbox": VM driver is one of: [virtualbox vmwarefusion kvm xhyve hyperv] ``` ### Options inherited from parent commands diff --git a/pkg/minikube/cluster/cluster.go b/pkg/minikube/cluster/cluster.go index 71881edd50d0..f1e398480ce2 100644 --- a/pkg/minikube/cluster/cluster.go +++ b/pkg/minikube/cluster/cluster.go @@ -380,6 +380,8 @@ func createHost(api libmachine.API, config MachineConfig) (*host.Host, error) { driver = createKVMHost(config) case "xhyve": driver = createXhyveHost(config) + case "hyperv": + driver = createHypervHost(config) default: glog.Exitf("Unsupported driver: %s\n", config.VMDriver) } diff --git a/pkg/minikube/cluster/cluster_non_windows_panic.go b/pkg/minikube/cluster/cluster_non_windows_panic.go new file mode 100644 index 000000000000..e3afdcb23289 --- /dev/null +++ b/pkg/minikube/cluster/cluster_non_windows_panic.go @@ -0,0 +1,25 @@ +// +build !windows + +/* +Copyright 2016 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 cluster + +import "github.com/docker/machine/libmachine/drivers" + +func createHypervHost(config MachineConfig) drivers.Driver { + panic("hyperv not supported") +} diff --git a/pkg/minikube/cluster/cluster_windows.go b/pkg/minikube/cluster/cluster_windows.go new file mode 100644 index 000000000000..ab7ba9730ab4 --- /dev/null +++ b/pkg/minikube/cluster/cluster_windows.go @@ -0,0 +1,32 @@ +/* +Copyright 2016 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 cluster + +import ( + "github.com/docker/machine/drivers/hyperv" + "github.com/docker/machine/libmachine/drivers" + "k8s.io/minikube/pkg/minikube/constants" +) + +func createHypervHost(config MachineConfig) drivers.Driver { + d := hyperv.NewDriver(constants.MachineName, constants.Minipath) + d.Boot2DockerURL = config.GetISOFileURI() + d.MemSize = config.Memory + d.CPU = config.CPUs + d.DiskSize = int(config.DiskSize) + return d +} diff --git a/pkg/minikube/constants/constants_gendocs.go b/pkg/minikube/constants/constants_gendocs.go index 46734d0a95de..6c34e66cc431 100644 --- a/pkg/minikube/constants/constants_gendocs.go +++ b/pkg/minikube/constants/constants_gendocs.go @@ -23,4 +23,5 @@ var SupportedVMDrivers = [...]string{ "vmwarefusion", "kvm", "xhyve", + "hyperv", } diff --git a/pkg/minikube/constants/constants_windows.go b/pkg/minikube/constants/constants_windows.go index d43062cb3d70..693ccfc5a33a 100644 --- a/pkg/minikube/constants/constants_windows.go +++ b/pkg/minikube/constants/constants_windows.go @@ -20,4 +20,5 @@ package constants var SupportedVMDrivers = [...]string{ "virtualbox", + "hyperv", } diff --git a/pkg/minikube/machine/client.go b/pkg/minikube/machine/client.go index 3b5f28b9d73c..c4eb4d168725 100644 --- a/pkg/minikube/machine/client.go +++ b/pkg/minikube/machine/client.go @@ -19,6 +19,7 @@ package machine import ( "os" + "github.com/docker/machine/drivers/hyperv" "github.com/docker/machine/drivers/virtualbox" "github.com/docker/machine/drivers/vmwarefusion" "github.com/docker/machine/libmachine/drivers/plugin" @@ -35,6 +36,8 @@ func StartDriver() { plugin.RegisterDriver(virtualbox.NewDriver("", "")) case "vmwarefusion": plugin.RegisterDriver(vmwarefusion.NewDriver("", "")) + case "hyperv": + plugin.RegisterDriver(hyperv.NewDriver("", "")) default: glog.Exitf("Unsupported driver: %s\n", driverName) } diff --git a/vendor/github.com/docker/machine/drivers/hyperv/hyperv.go b/vendor/github.com/docker/machine/drivers/hyperv/hyperv.go new file mode 100644 index 000000000000..e67e04371176 --- /dev/null +++ b/vendor/github.com/docker/machine/drivers/hyperv/hyperv.go @@ -0,0 +1,476 @@ +package hyperv + +import ( + "fmt" + "net" + "os" + "time" + + "errors" + + "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 +} + +const ( + defaultDiskSize = 20000 + defaultMemory = 1024 + defaultCPU = 1 + defaultVLanID = 0 +) + +// NewDriver creates a new Hyper-v driver with default settings. +func NewDriver(hostName, storePath string) *Driver { + return &Driver{ + DiskSize: defaultDiskSize, + MemSize: defaultMemory, + CPU: defaultCPU, + 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", + }, + } +} + +func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { + if drivers.EngineInstallURLFlagSet(flags) { + return errors.New("--engine-install-url cannot be used with the hyperv driver, use --hyperv-boot2docker-url instead") + } + 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.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("(", "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) + if err := b2dutils.UpdateISOCache(d.Boot2DockerURL); err != nil { + return err + } + + return nil +} + +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...") + virtualSwitch, err := d.chooseVirtualSwitch() + if err != nil { + return err + } + + log.Infof("Using switch %q", virtualSwitch) + + diskImage, err := d.generateDiskImage() + if err != nil { + return err + } + + if err := cmd("New-VM", + d.MachineName, + "-Path", fmt.Sprintf("'%s'", d.ResolveStorePath(".")), + "-SwitchName", quote(virtualSwitch), + "-MemoryStartupBytes", toMb(d.MemSize)); err != nil { + return err + } + + if d.CPU > 1 { + if err := cmd("Set-VMProcessor", + d.MachineName, + "-Count", fmt.Sprintf("%d", d.CPU)); err != nil { + return err + } + } + + if d.MacAddr != "" { + if err := cmd("Set-VMNetworkAdapter", + "-VMName", d.MachineName, + "-StaticMacAddress", fmt.Sprintf("\"%s\"", d.MacAddr)); err != nil { + return err + } + } + + if d.VLanID > 0 { + if err := cmd("Set-VMNetworkAdapterVlan", + "-VMName", d.MachineName, + "-Access", + "-VlanId", fmt.Sprintf("%d", d.VLanID)); err != nil { + return err + } + } + + if err := cmd("Set-VMDvdDrive", + "-VMName", d.MachineName, + "-Path", quote(d.ResolveStorePath("boot2docker.iso"))); err != nil { + return err + } + + if err := cmd("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) { + stdout, err := cmdOut("(Get-VMSwitch).Name") + if err != nil { + return "", err + } + + switches := parseLines(stdout) + + if d.VSwitch == "" { + if len(switches) < 1 { + return "", fmt.Errorf("no vswitch found. A valid vswitch must be available for this command to run. Check https://docs.docker.com/machine/drivers/hyper-v/") + } + + return switches[0], nil + } + + found := false + for _, name := range switches { + if name == d.VSwitch { + found = true + break + } + } + + if !found { + return "", fmt.Errorf("vswitch %q not found", d.VSwitch) + } + + return d.VSwitch, nil +} + +// waitForIP waits until the host has a valid IP +func (d *Driver) waitForIP() (string, error) { + log.Infof("Waiting for host to start...") + + for { + ip, _ := d.GetIP() + if ip != "" { + return ip, nil + } + + 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("Start-VM", d.MachineName); err != nil { + return err + } + + ip, err := d.waitForIP() + if err != nil { + return err + } + + d.IPAddress = ip + + return nil +} + +// Stop stops an host +func (d *Driver) Stop() error { + if err := cmd("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("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("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("((", "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 priviledges + // 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("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() + + file.Seek(0, os.SEEK_SET) + _, err = file.Write(tarBuf.Bytes()) + if err != nil { + return "", err + } + file.Close() + + if err := cmd("Convert-VHD", "-Path", quote(fixed), "-DestinationPath", quote(diskImage), "-VHDType", "Dynamic", "-DeleteSource"); err != nil { + return "", err + } + + if isWindowsAdmin { + if err := cmd("Resize-VHD", "-Path", quote(diskImage), "-SizeBytes", toMb(d.DiskSize)); err != nil { + return "", err + } + } + + return diskImage, nil +} diff --git a/vendor/github.com/docker/machine/drivers/hyperv/powershell.go b/vendor/github.com/docker/machine/drivers/hyperv/powershell.go new file mode 100644 index 000000000000..b486cbad6e62 --- /dev/null +++ b/vendor/github.com/docker/machine/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-Command Get-VM).ModuleName") + if err != nil { + return err + } + + resp := parseLines(stdout) + if 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("Hyper-V Administrators")`) + 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) +}