mirror of
https://github.com/linuxkit/linuxkit.git
synced 2025-07-20 17:49:10 +00:00
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:
parent
309ae23c2e
commit
a42a3ffb39
17
src/cmd/linuxkit/hyperv_fallback.go
Normal file
17
src/cmd/linuxkit/hyperv_fallback.go
Normal 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() {
|
||||
}
|
101
src/cmd/linuxkit/hyperv_windows.go
Normal file
101
src/cmd/linuxkit/hyperv_windows.go
Normal 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)
|
||||
}
|
@ -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.")
|
||||
}
|
||||
|
263
src/cmd/linuxkit/run_hyperv.go
Normal file
263
src/cmd/linuxkit/run_hyperv.go
Normal 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")
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user