diff --git a/src/cmd/linuxkit/hyperv_fallback.go b/src/cmd/linuxkit/hyperv_fallback.go new file mode 100644 index 000000000..723649175 --- /dev/null +++ b/src/cmd/linuxkit/hyperv_fallback.go @@ -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() { +} diff --git a/src/cmd/linuxkit/hyperv_windows.go b/src/cmd/linuxkit/hyperv_windows.go new file mode 100644 index 000000000..2d71b6012 --- /dev/null +++ b/src/cmd/linuxkit/hyperv_windows.go @@ -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) +} diff --git a/src/cmd/linuxkit/run.go b/src/cmd/linuxkit/run.go index 11636ba78..7e2e9c465 100644 --- a/src/cmd/linuxkit/run.go +++ b/src/cmd/linuxkit/run.go @@ -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.") } diff --git a/src/cmd/linuxkit/run_hyperv.go b/src/cmd/linuxkit/run_hyperv.go new file mode 100644 index 000000000..6794ffd2e --- /dev/null +++ b/src/cmd/linuxkit/run_hyperv.go @@ -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") +} diff --git a/src/cmd/linuxkit/util.go b/src/cmd/linuxkit/util.go index edacdf1fc..f7a8b1faf 100644 --- a/src/cmd/linuxkit/util.go +++ b/src/cmd/linuxkit/util.go @@ -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