Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 124 additions & 18 deletions drivers/hyperv/hyperv.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package hyperv

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"regexp"
"strings"
"time"

Expand All @@ -19,13 +23,15 @@ import (
type Driver struct {
*drivers.BaseDriver
Boot2DockerURL string
WindowsVHDUrl string
VSwitch string
DiskSize int
MemSize int
CPU int
MacAddr string
VLanID int
DisableDynamicMemory bool
OS string
}

const (
Expand All @@ -35,6 +41,7 @@ const (
defaultVLanID = 0
defaultDisableDynamicMemory = false
defaultSwitchID = "c08cb7b8-9b3c-408e-8e30-5e16a3aeb444"
defaultServerImageFilename = "hybrid-minikube-windows-server.vhdx"
)

// NewDriver creates a new Hyper-v driver with default settings.
Expand All @@ -43,6 +50,7 @@ func NewDriver(hostName, storePath string) *Driver {
DiskSize: defaultDiskSize,
MemSize: defaultMemory,
CPU: defaultCPU,
WindowsVHDUrl: mcnutils.ConfigGuest.GetVHDUrl(),
DisableDynamicMemory: defaultDisableDynamicMemory,
BaseDriver: &drivers.BaseDriver{
MachineName: hostName,
Expand Down Expand Up @@ -142,7 +150,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, fmt.Errorf("failed to find the VM status")
}

resp := parseLines(stdout)
Expand Down Expand Up @@ -186,20 +194,35 @@ func (d *Driver) PreCreateCheck() error {
return err
}

// Downloading boot2docker to cache should be done here to make sure
// Downloading boot2docker/windows-server 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)

if mcnutils.ConfigGuest.GetGuestOS() != "windows" {
err = b2dutils.UpdateISOCache(d.Boot2DockerURL)
} else {
err = b2dutils.UpdateVHDCache(d.WindowsVHDUrl)
}

return err
}

func (d *Driver) Create() error {
b2dutils := mcnutils.NewB2dUtils(d.StorePath)
if err := b2dutils.CopyIsoToMachineDir(d.Boot2DockerURL, d.MachineName); err != nil {
return err

if mcnutils.ConfigGuest.GetGuestOS() == "windows" {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be nice if the ConfigGuest type had a "IsWindows()" method that we reuse throughout this change. I know it's just a comparison against a string, but this type of comparison changes over time.

d.SSHUser = "Administrator"
if err := b2dutils.CopyWindowsVHDToMachineDir(d.WindowsVHDUrl, d.MachineName); err != nil {
return err
}
} else {
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
}
Expand All @@ -214,18 +237,36 @@ func (d *Driver) Create() error {
}
log.Infof("Using switch %q", d.VSwitch)

diskImage, err := d.generateDiskImage()
if mcnutils.ConfigGuest.GetGuestOS() == "windows" {
log.Infof("Adding SSH key to the VHDX...")
if err := writeSSHKeyToVHDX(d.ResolveStorePath(defaultServerImageFilename), d.publicSSHKeyPath()); err != nil {
log.Errorf("Error creating disk image: %s", err)
return err
}
}

var diskImage string
var err error
if mcnutils.ConfigGuest.GetGuestOS() != "windows" {
diskImage, err = d.generateDiskImage()
}
if err != nil {
return err
}

vmGeneration := "1"
if mcnutils.ConfigGuest.GetGuestOS() == "windows" {
vmGeneration = "2"
}

if err := cmd("Hyper-V\\New-VM",
d.MachineName,
"-Path", fmt.Sprintf("'%s'", d.ResolveStorePath(".")),
"-SwitchName", quote(d.VSwitch),
"-Generation", quote(vmGeneration),
"-MemoryStartupBytes", toMb(d.MemSize)); err != nil {
return err
}

if d.DisableDynamicMemory {
if err := cmd("Hyper-V\\Set-VMMemory",
"-VMName", d.MachineName,
Expand All @@ -237,7 +278,8 @@ func (d *Driver) Create() error {
if d.CPU > 1 {
if err := cmd("Hyper-V\\Set-VMProcessor",
d.MachineName,
"-Count", fmt.Sprintf("%d", d.CPU)); err != nil {
"-Count", fmt.Sprintf("%d", d.CPU),
"-ExposeVirtualizationExtensions", "$true"); err != nil {
return err
}
}
Expand All @@ -259,19 +301,29 @@ func (d *Driver) Create() error {
}
}

if err := cmd("Hyper-V\\Set-VMDvdDrive",
"-VMName", d.MachineName,
"-Path", quote(d.ResolveStorePath("boot2docker.iso"))); err != nil {
return err
if mcnutils.ConfigGuest.GetGuestOS() != "windows" {
log.Infof("Attaching ISO and disk...")
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
if mcnutils.ConfigGuest.GetGuestOS() == "windows" {
if err := cmd("Hyper-V\\Add-VMHardDiskDrive",
"-VMName", d.MachineName,
"-Path", quote(d.ResolveStorePath("hybrid-minikube-windows-server.vhdx")),
"-ControllerType", "SCSI"); err != nil {
return err
}
} else {
if err := cmd("Hyper-V\\Add-VMHardDiskDrive",
"-VMName", d.MachineName,
"-Path", quote(diskImage)); err != nil {
return err
}
}

log.Infof("Starting VM...")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a useful logging message we're removing. Could even enhance it a bit with info about the VM we're starting.

return d.Start()
}

Expand Down Expand Up @@ -507,3 +559,57 @@ func (d *Driver) generateDiskImage() (string, error) {

return diskImage, nil
}
func writeSSHKeyToVHDX(vhdxPath, publicSSHKeyPath string) (retErr error) {
output, err := cmdOut(
"powershell", "-Command",
"(Get-DiskImage -ImagePath", quote(vhdxPath), "| Mount-DiskImage -PassThru) | Out-Null;",
"$diskNumber = (Get-DiskImage -ImagePath", quote(vhdxPath), "| Get-Disk).Number;",
"Set-Disk -Number $diskNumber -IsReadOnly $false;",
"(Get-Disk -Number $diskNumber | Get-Partition | Get-Volume).DriveLetter",
)
if err != nil {
return fmt.Errorf("failed to mount VHDX and retrieve mount directory: %w", err)
}

regex := regexp.MustCompile(`\s+|\r|\n`)
driveLetter := regex.ReplaceAllString(output, "")

if driveLetter == "" {
log.Debugf("No drive letter assigned to VHDX")
return errors.New("no drive letter assigned to VHDX")
}

mountDir := strings.TrimSpace(driveLetter) + ":" + string(os.PathSeparator)

defer func() {
if unmountErr := cmd("Dismount-DiskImage", "-ImagePath", quote(vhdxPath)); unmountErr != nil {
retErr = errors.Join(retErr, fmt.Errorf("failed to unmount VHDX: %w", unmountErr))
}
}()

sshDir := filepath.Join(mountDir, "ProgramData", "ssh")
adminAuthKeys := filepath.Join(sshDir, "administrators_authorized_keys")

pubKey, err := os.ReadFile(publicSSHKeyPath)
if err != nil {
return fmt.Errorf("failed to read public SSH key from %s: %w", publicSSHKeyPath, err)
}

if _, err := os.Stat(mountDir); os.IsNotExist(err) {
return fmt.Errorf("mount point %s does not exist", mountDir)
}

if err := os.MkdirAll(sshDir, 0755); err != nil {
return fmt.Errorf("failed to create SSH directory: %w", err)
}

if err := ioutil.WriteFile(adminAuthKeys, pubKey, 0644); err != nil {
return fmt.Errorf("failed to write public key: %w", err)
}

if err := cmd("icacls.exe", quote(adminAuthKeys), "/inheritance:r", "/grant", "Administrators:F", "/grant", "SYSTEM:F"); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", adminAuthKeys, err)
}

return nil
}
4 changes: 2 additions & 2 deletions drivers/hyperv/powershell.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func hypervAvailable() error {
}

resp := parseLines(stdout)
if resp == nil || len(resp) == 0 || resp[0] != "Hyper-V" {
if len(resp) == 0 || resp[0] != "Hyper-V" {
return ErrNotInstalled
}

Expand Down Expand Up @@ -106,7 +106,7 @@ func isWindowsAdministrator() (bool, error) {
}

func quote(text string) string {
return fmt.Sprintf("'%s'", text)
return fmt.Sprintf(`"%s"`, text)
}

func toMb(value int) string {
Expand Down
4 changes: 2 additions & 2 deletions libmachine/examples/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func create() {
return
}

h, err := client.NewHost("virtualbox", data)
h, err := client.NewHost("virtualbox", "linux", data)
if err != nil {
log.Error(err)
return
Expand Down Expand Up @@ -82,7 +82,7 @@ func streaming() {
return
}

h, err := client.NewHost("virtualbox", data)
h, err := client.NewHost("virtualbox", "linux", data)
if err != nil {
log.Error(err)
return
Expand Down
7 changes: 7 additions & 0 deletions libmachine/host/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type Host struct {
HostOptions *Options
Name string
RawDriver []byte `json:"-"`
Guest Guest
}

type Options struct {
Expand All @@ -61,6 +62,12 @@ type Metadata struct {
HostOptions Options
}

type Guest struct {
Name string
Version string
URL string
}

func ValidateHostName(name string) bool {
return validHostNamePattern.MatchString(name)
}
Expand Down
Loading