cmd: Add initial Hyper-V run backend

The Hyper-V backend is loosly based on the docker-machine code
as well as ./scripts/LinuxKit.ps1. It shells out to Powershell
for most of the configuration.

Console is provided by github.com/Azure/go-ansiterm/winterm
and the ode surrounding it is loosely based on the equivalent
code in containerd and moby/moby.

Signed-off-by: Rolf Neugebauer <rolf.neugebauer@docker.com>
This commit is contained in:
Rolf Neugebauer 2017-06-09 14:21:34 +01:00
parent 309ae23c2e
commit a42a3ffb39
5 changed files with 399 additions and 0 deletions

View File

@ -0,0 +1,17 @@
// +build !windows
package main
// Fallback implementation
import (
"log"
)
func hypervStartConsole(vmName string) error {
log.Fatalf("This function should not be called")
return nil
}
func hypervRestoreConsole() {
}

View File

@ -0,0 +1,101 @@
package main
// Implement Windows specific functions here
import (
"fmt"
"io"
"net"
"os"
"time"
"github.com/Azure/go-ansiterm/winterm"
"github.com/Microsoft/go-winio"
log "github.com/Sirupsen/logrus"
)
// Some of the code below is copied and modified from:
// https://github.com/moby/moby/blob/master/pkg/term/term_windows.go
const (
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx
enableVirtualTerminalInput = 0x0200
enableVirtualTerminalProcessing = 0x0004
disableNewlineAutoReturn = 0x0008
)
func hypervStartConsole(vmName string) error {
if err := hypervConfigureConsole(); err != nil {
log.Infof("Configure Console: %v", err)
}
pipeName := fmt.Sprintf(`\\.\pipe\%s-com1`, vmName)
var c net.Conn
var err error
for count := 1; count < 100; count++ {
c, err = winio.DialPipe(pipeName, nil)
defer c.Close()
if err != nil {
// Argh, different Windows versions seem to
// return different errors and we can't easily
// catch the error. On some versions it is
// winio.ErrTimeout...
// Instead poll 100 times and then error out
log.Infof("Connect to console: %v", err)
time.Sleep(10 * 1000 * 1000 * time.Nanosecond)
continue
}
break
}
if err != nil {
return err
}
log.Info("Connected")
go io.Copy(c, os.Stdin)
_, err = io.Copy(os.Stdout, c)
if err != nil {
return err
}
return nil
}
var (
hypervStdinMode uint32
hypervStdoutMode uint32
hypervStderrMode uint32
)
func hypervConfigureConsole() error {
// Turn on VT handling on all std handles, if possible. This might
// fail on older windows version, but we'll ignore that for now
// Also disable local echo
fd := os.Stdin.Fd()
if hypervStdinMode, err := winterm.GetConsoleMode(fd); err == nil {
if err = winterm.SetConsoleMode(fd, hypervStdinMode|enableVirtualTerminalInput); err != nil {
log.Warn("VT Processing is not supported on stdin")
}
}
fd = os.Stdout.Fd()
if hypervStdoutMode, err := winterm.GetConsoleMode(fd); err == nil {
if err = winterm.SetConsoleMode(fd, hypervStdoutMode|enableVirtualTerminalProcessing|disableNewlineAutoReturn); err != nil {
log.Warn("VT Processing is not supported on stdout")
}
}
fd = os.Stderr.Fd()
if hypervStderrMode, err := winterm.GetConsoleMode(fd); err == nil {
if err = winterm.SetConsoleMode(fd, hypervStderrMode|enableVirtualTerminalProcessing|disableNewlineAutoReturn); err != nil {
log.Warn("VT Processing is not supported on stderr")
}
}
return nil
}
func hypervRestoreConsole() {
winterm.SetConsoleMode(os.Stdin.Fd(), hypervStdinMode)
winterm.SetConsoleMode(os.Stdout.Fd(), hypervStdoutMode)
winterm.SetConsoleMode(os.Stderr.Fd(), hypervStderrMode)
}

View File

@ -21,6 +21,7 @@ func runUsage() {
fmt.Printf(" azure\n")
fmt.Printf(" gcp\n")
fmt.Printf(" hyperkit [macOS]\n")
fmt.Printf(" hyperv [Windows]\n")
fmt.Printf(" packet\n")
fmt.Printf(" qemu [linux]\n")
fmt.Printf(" vcenter\n")
@ -51,6 +52,8 @@ func run(args []string) {
os.Exit(0)
case "hyperkit":
runHyperKit(args[1:])
case "hyperv":
runHyperV(args[1:])
case "packet":
runPacket(args[1:])
case "qemu":
@ -65,6 +68,8 @@ func run(args []string) {
runHyperKit(args)
case "linux":
runQemu(args)
case "windows":
runHyperV(args)
default:
log.Errorf("There currently is no default 'run' backend for your platform.")
}

View File

@ -0,0 +1,263 @@
package main
import (
"bytes"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
log "github.com/Sirupsen/logrus"
)
// Process the run arguments and execute run
func runHyperV(args []string) {
flags := flag.NewFlagSet("hyperv", flag.ExitOnError)
invoked := filepath.Base(os.Args[0])
flags.Usage = func() {
fmt.Printf("USAGE: %s run hyperv [options] path\n\n", invoked)
fmt.Printf("'path' specifies the path to a EFI ISO file.\n")
fmt.Printf("\n")
fmt.Printf("Options:\n")
flags.PrintDefaults()
}
keep := flags.Bool("keep", false, "Keep the VM after finishing")
vmName := flags.String("name", "", "Name of the Hyper-V VM")
cpus := flags.Int("cpus", 1, "Number of CPUs")
mem := flags.Int("mem", 1024, "Amount of memory in MB")
var disks Disks
flags.Var(&disks, "disk", "Disk config. [file=]path[,size=1G]")
switchName := flags.String("switch", "", "Which Hyper-V switch to attache the VM to. If left empty, the first external switch found is used.")
if err := flags.Parse(args); err != nil {
log.Fatal("Unable to parse args")
}
remArgs := flags.Args()
if len(remArgs) == 0 {
fmt.Println("Please specify the path to the ISO image to boot\n")
flags.Usage()
os.Exit(1)
}
isoPath := remArgs[0]
// Sanity checks. Errors out on failure
hypervChecks()
vmSwitch, err := hypervGetSwitch(*switchName)
if err != nil {
log.Fatalf("%v", err)
}
log.Debugf("Using switch: %s", vmSwitch)
if *vmName == "" {
*vmName = filepath.Base(isoPath)
*vmName = strings.TrimSuffix(*vmName, ".iso")
// Also strip -efi in case it is present
*vmName = strings.TrimSuffix(*vmName, "-efi")
}
log.Infof("Creating VM: %s", *vmName)
_, out, err := poshCmd("New-VM", "-Name", fmt.Sprintf("'%s'", *vmName),
"-Generation", "2",
"-NoVHD",
"-SwitchName", fmt.Sprintf("'%s'", vmSwitch))
if err != nil {
log.Fatalf("Failed to create new VM: %v\n%s", err, out)
}
log.Infof("Configure VM: %s", *vmName)
_, out, err = poshCmd("Set-VM", "-Name", fmt.Sprintf("'%s'", *vmName),
"-AutomaticStartAction", "Nothing",
"-AutomaticStopAction", "ShutDown",
"-CheckpointType", "Disabled",
"-MemoryStartupBytes", fmt.Sprintf("%dMB", *mem),
"-StaticMemory",
"-ProcessorCount", fmt.Sprintf("%d", *cpus))
if err != nil {
log.Fatalf("Failed to configure new VM: %v\n%s", err, out)
}
for i, d := range disks {
id := ""
if i != 0 {
id = strconv.Itoa(i)
}
if d.Size != 0 && d.Path == "" {
d.Path = *vmName + "-disk" + id + ".vhdx"
}
if d.Path == "" {
log.Fatalf("disk specified with no size or name")
}
if _, err := os.Stat(d.Path); err != nil {
if os.IsNotExist(err) {
log.Infof("Creating new disk %s %dMB", d.Path, d.Size)
_, out, err = poshCmd("New-VHD",
"-Path", fmt.Sprintf("'%s'", d.Path),
"-SizeBytes", fmt.Sprintf("%dMB", d.Size),
"-Dynamic")
if err != nil {
log.Fatalf("Failed to create VHD %s: %v\n%s", d.Path, err, out)
}
} else {
log.Fatalf("Problem accessing disk %s. %v", d.Path, err)
}
} else {
log.Infof("Using existing disk %s", d.Path)
}
_, out, err = poshCmd("Add-VMHardDiskDrive",
"-VMName", fmt.Sprintf("'%s'", *vmName),
"-Path", fmt.Sprintf("'%s'", d.Path))
if err != nil {
log.Fatalf("Failed to add VHD %s: %v\n%s", d.Path, err, out)
}
}
log.Info("Setting up boot from ISO")
_, out, err = poshCmd("Add-VMDvdDrive",
"-VMName", fmt.Sprintf("'%s'", *vmName),
"-Path", fmt.Sprintf("'%s'", isoPath))
if err != nil {
log.Fatalf("Failed add DVD: %v\n%s", err, out)
}
_, out, err = poshCmd(
fmt.Sprintf("$cdrom = Get-VMDvdDrive -vmname '%s';", *vmName),
"Set-VMFirmware", "-VMName", fmt.Sprintf("'%s'", *vmName),
"-EnableSecureBoot", "Off",
"-FirstBootDevice", "$cdrom")
if err != nil {
log.Fatalf("Failed set DVD as boot device: %v\n%s", err, out)
}
log.Info("Set up COM port")
_, out, err = poshCmd("Set-VMComPort",
"-VMName", fmt.Sprintf("'%s'", *vmName),
"-number", "1",
"-Path", fmt.Sprintf(`\\.\pipe\%s-com1`, *vmName))
if err != nil {
log.Fatalf("Failed set up COM port: %v\n%s", err, out)
}
log.Info("Start the VM")
_, out, err = poshCmd("Start-VM", "-Name", fmt.Sprintf("'%s'", *vmName))
if err != nil {
log.Fatalf("Failed start the VM: %v\n%s", err, out)
}
err = hypervStartConsole(*vmName)
if err != nil {
log.Infof("Console returned: %v\n", err)
}
hypervRestoreConsole()
if *keep {
return
}
log.Info("Stop the VM")
_, out, err = poshCmd("Stop-VM",
"-Name", fmt.Sprintf("'%s'", *vmName), "-Force")
if err != nil {
// Don't error out, could get an error if VM is already stopped
log.Infof("Stop-VM error: %v\n%s", err, out)
}
log.Info("Remove the VM")
_, out, err = poshCmd("Remove-VM",
"-Name", fmt.Sprintf("'%s'", *vmName), "-Force")
if err != nil {
log.Infof("Remove-VM error: %v\n%s", err, out)
}
}
var powershell string
// Execute a powershell command
func poshCmd(args ...string) (string, string, error) {
args = append([]string{"-NoProfile", "-NonInteractive"}, args...)
cmd := exec.Command(powershell, args...)
log.Debugf("[POSH]: %s %s", powershell, strings.Join(args, " "))
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
return stdout.String(), stderr.String(), err
}
// Perform some sanity checks, and error if failing
func hypervChecks() {
powershell, _ = exec.LookPath("powershell.exe")
if powershell == "" {
log.Fatalf("Could not find powershell executable")
}
hvAdmin := false
admin := false
out, _, err := poshCmd(`@([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Hyper-V Administrators")`)
if err != nil {
log.Debugf("Check for Hyper-V Admin failed: %v", err)
}
res := splitLines(out)
if res[0] == "True" {
hvAdmin = true
}
out, _, err = poshCmd(`@([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")`)
if err != nil {
log.Debugf("Check for Admin failed: %v", err)
}
res = splitLines(out)
if res[0] == "True" {
admin = true
}
if !hvAdmin && !admin {
log.Fatal("Must be run from an elevated prompt or user must be in the Hyper-V Administrator role")
}
out, _, err = poshCmd("@(Get-Command Get-VM).ModuleName")
if err != nil {
log.Fatalf("Check for Hyper-V powershell modules failed: %v")
}
res = splitLines(out)
if res[0] != "Hyper-V" {
log.Fatal("The Hyper-V powershell module does not seem to be installed")
}
}
// Find a Hyper-V switch. Either check that the supplied switch exists
// or find the first external switch.
func hypervGetSwitch(name string) (string, error) {
if name != "" {
_, _, err := poshCmd("Get-VMSwitch", name)
if err != nil {
return "", fmt.Errorf("Could not find switch %s: %v", name, err)
}
return name, nil
}
out, _, err := poshCmd("get-vmswitch | Format-Table -Property Name, SwitchType -HideTableHeaders")
if err != nil {
return "", fmt.Errorf("Could not get list of switches: %v", err)
}
switches := splitLines(out)
for _, s := range switches {
if len(s) == 0 {
continue
}
t := strings.SplitN(s, " ", 2)
if len(t) < 2 {
continue
}
if strings.Contains(t[1], "External") {
return strings.Trim(t[0], " "), nil
}
}
return "", fmt.Errorf("Could not find an external switch")
}

View File

@ -1,6 +1,7 @@
package main
import (
"bufio"
"fmt"
"os"
"strconv"
@ -99,6 +100,18 @@ func stringToIntArray(l string, sep string) ([]int, error) {
return i, nil
}
// Convert a multi-line string into an array of strings
func splitLines(in string) []string {
res := []string{}
s := bufio.NewScanner(strings.NewReader(in))
for s.Scan() {
res = append(res, s.Text())
}
return res
}
// This function parses the "size" parameter of a disk specification
// and returns the size in MB. The "size" paramter defaults to GB, but
// the unit can be explicitly set with either a G (for GB) or M (for