Skip to content
This repository was archived by the owner on Jan 8, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 9 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
134 changes: 116 additions & 18 deletions drivers/hyperv/hyperv.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package hyperv
import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"os"
"strings"
Expand All @@ -19,13 +20,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 +38,8 @@ const (
defaultVLanID = 0
defaultDisableDynamicMemory = false
defaultSwitchID = "c08cb7b8-9b3c-408e-8e30-5e16a3aeb444"
defaultWindowsServerVHD = "https://minikubevhdimagebuider.blob.core.windows.net/minikubevhdimage/WIN-SER-2025.vhdx"
Comment thread
bobsira marked this conversation as resolved.
Outdated
defaultServerImageFilename = "WIN-SER-2025.vhdx"
)

// NewDriver creates a new Hyper-v driver with default settings.
Expand All @@ -43,6 +48,7 @@ func NewDriver(hostName, storePath string) *Driver {
DiskSize: defaultDiskSize,
MemSize: defaultMemory,
CPU: defaultCPU,
WindowsVHDUrl: defaultWindowsServerVHD,
DisableDynamicMemory: defaultDisableDynamicMemory,
BaseDriver: &drivers.BaseDriver{
MachineName: hostName,
Expand Down Expand Up @@ -142,7 +148,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 +192,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.ConfigGuestOSUtil.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.ConfigGuestOSUtil.GetGuestOS() == "windows" {
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 +235,36 @@ func (d *Driver) Create() error {
}
log.Infof("Using switch %q", d.VSwitch)

diskImage, err := d.generateDiskImage()
if mcnutils.ConfigGuestOSUtil.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.ConfigGuestOSUtil.GetGuestOS() != "windows" {
diskImage, err = d.generateDiskImage()
}
if err != nil {
return err
}

vmGeneration := "1"
if mcnutils.ConfigGuestOSUtil.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 +276,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 +299,30 @@ 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.ConfigGuestOSUtil.GetGuestOS() == "windows" {
Comment thread
bobsira marked this conversation as resolved.
Outdated
// === Windows ===
} else {
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.ConfigGuestOSUtil.GetGuestOS() == "windows" {
if err := cmd("Hyper-V\\Add-VMHardDiskDrive",
"-VMName", d.MachineName,
"-Path", quote(d.ResolveStorePath("WIN-SER-2025.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...")
Copy link
Copy Markdown

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 +558,50 @@ func (d *Driver) generateDiskImage() (string, error) {

return diskImage, nil
}

func writeSSHKeyToVHDX(vhdxPath, publicSSHKeyPath string) error {
mountDir := "E:\\"
Comment thread
bobsira marked this conversation as resolved.
Outdated
sshDir := mountDir + "ProgramData\\ssh\\"
adminAuthKeys := sshDir + "administrators_authorized_keys"

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

err = cmd("Mount-DiskImage", "-ImagePath", quote(vhdxPath), "-StorageType", "VHDX", "-PassThru", "|",
"Get-Disk", "|", "Set-Disk", "-IsReadOnly", "$false")
if err != nil {
return fmt.Errorf("failed to mount VHDX: %w", err)
}
defer func() {
// Ensure unmounting even in case of failure
unmountErr := cmd("Dismount-DiskImage", "-ImagePath", quote(vhdxPath))
if unmountErr != nil {
log.Warnf("Failed to unmount VHDX: %v", unmountErr)
}
}()

// Wait for mount stability
time.Sleep(2 * time.Second)

// Ensure mount directory exists
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)
}

err = cmd("icacls.exe", quote(adminAuthKeys), "/inheritance:r", "/grant", "Administrators:F", "/grant", "SYSTEM:F")
Comment thread
bobsira marked this conversation as resolved.
Outdated
if err != nil {
return fmt.Errorf("failed to set permissions: %w", err)
}

return nil
}
4 changes: 4 additions & 0 deletions libmachine/drivers/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ func GetSSHClientFromDriver(d Driver) (ssh.Client, error) {
}

func RunSSHCommandFromDriver(d Driver, command string) (string, error) {
// if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" {
// return "", fmt.Errorf("SSH commands are not supported on Windows")
// }

client, err := GetSSHClientFromDriver(d)
if err != nil {
return "", err
Expand Down
1 change: 1 addition & 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:"-"`
GuestOS string
}

type Options struct {
Expand Down
30 changes: 18 additions & 12 deletions libmachine/libmachine.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"github.com/docker/machine/libmachine/check"
"github.com/docker/machine/libmachine/drivers"
"github.com/docker/machine/libmachine/drivers/plugin/localbinary"
"github.com/docker/machine/libmachine/drivers/rpc"
rpcdriver "github.com/docker/machine/libmachine/drivers/rpc"
"github.com/docker/machine/libmachine/engine"
"github.com/docker/machine/libmachine/host"
"github.com/docker/machine/libmachine/log"
Expand All @@ -28,7 +28,8 @@ import (

type API interface {
io.Closer
NewHost(driverName string, rawDriver []byte) (*host.Host, error)
NewHost(driverName string, guestOS string, rawDriver []byte) (*host.Host, error)
DefineGuest(h *host.Host)
Create(h *host.Host) error
persist.Store
GetMachinesDir() string
Expand All @@ -53,7 +54,7 @@ func NewClient(storePath, certsDir string) *Client {
}
}

func (api *Client) NewHost(driverName string, rawDriver []byte) (*host.Host, error) {
func (api *Client) NewHost(driverName string, guestOS string, rawDriver []byte) (*host.Host, error) {
driver, err := api.clientDriverFactory.NewRPCClientDriver(driverName, rawDriver)
if err != nil {
return nil, err
Expand All @@ -64,6 +65,7 @@ func (api *Client) NewHost(driverName string, rawDriver []byte) (*host.Host, err
Name: driver.GetMachineName(),
Driver: driver,
DriverName: driver.DriverName(),
GuestOS: guestOS,
HostOptions: &host.Options{
AuthOptions: &auth.Options{
CertDir: api.certsDir,
Expand Down Expand Up @@ -113,11 +115,15 @@ func (api *Client) Load(name string) (*host.Host, error) {
return h, nil
}

func (api *Client) DefineGuest(h *host.Host) {
mcnutils.SetGuestOSUtil(h.GuestOS)
}

// Create is the wrapper method which covers all of the boilerplate around
// actually creating, provisioning, and persisting an instance in the store.
func (api *Client) Create(h *host.Host) error {
if err := cert.BootstrapCertificates(h.AuthOptions()); err != nil {
return fmt.Errorf("Error generating certificates: %s", err)
return fmt.Errorf("error generating certificates: %s", err)
}

log.Info("Running pre-create checks...")
Expand All @@ -129,13 +135,13 @@ func (api *Client) Create(h *host.Host) error {
}

if err := api.Save(h); err != nil {
return fmt.Errorf("Error saving host to store before attempting creation: %s", err)
return fmt.Errorf("error saving host to store before attempting creation: %s", err)
}

log.Info("Creating machine...")

if err := api.performCreate(h); err != nil {
return fmt.Errorf("Error creating machine: %s", err)
return fmt.Errorf("error creating machine: %s", err)
}

log.Debug("Reticulating splines...")
Expand All @@ -145,11 +151,11 @@ func (api *Client) Create(h *host.Host) error {

func (api *Client) performCreate(h *host.Host) error {
if err := h.Driver.Create(); err != nil {
return fmt.Errorf("Error in driver during machine creation: %s", err)
return fmt.Errorf("error in driver during machine creation: %s", err)
}

if err := api.Save(h); err != nil {
return fmt.Errorf("Error saving host to store after attempting creation: %s", err)
return fmt.Errorf("error saving host to store after attempting creation: %s", err)
}

// TODO: Not really a fan of just checking "none" or "ci-test" here.
Expand All @@ -159,24 +165,24 @@ func (api *Client) performCreate(h *host.Host) error {

log.Info("Waiting for machine to be running, this may take a few minutes...")
if err := mcnutils.WaitFor(drivers.MachineInState(h.Driver, state.Running)); err != nil {
return fmt.Errorf("Error waiting for machine to be running: %s", err)
return fmt.Errorf("error waiting for machine to be running: %s", err)
}

log.Info("Detecting operating system of created instance...")
provisioner, err := provision.DetectProvisioner(h.Driver)
if err != nil {
return fmt.Errorf("Error detecting OS: %s", err)
return fmt.Errorf("error detecting OS: %s", err)
}

log.Infof("Provisioning with %s...", provisioner.String())
if err := provisioner.Provision(*h.HostOptions.SwarmOptions, *h.HostOptions.AuthOptions, *h.HostOptions.EngineOptions); err != nil {
return fmt.Errorf("Error running provisioning: %s", err)
return fmt.Errorf("error running provisioning: %s", err)
}

// We should check the connection to docker here
log.Info("Checking connection to Docker...")
if _, _, err = check.DefaultConnChecker.Check(h, false); err != nil {
return fmt.Errorf("Error checking the host: %s", err)
return fmt.Errorf("error checking the host: %s", err)
}

log.Info("Docker is up and running!")
Expand Down
Loading