From a291fb282abc9d19fd3f54d66dc5fdc6e8849284 Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Fri, 20 Sep 2024 19:38:37 +0100 Subject: [PATCH 01/16] inital windows node setup --- drivers/hyperv/hyperv.go | 115 ++++++++++++++++++++++++++++------- libmachine/host/host.go | 1 + libmachine/libmachine.go | 30 +++++---- libmachine/mcnutils/b2d.go | 38 ++++++++++-- libmachine/mcnutils/utils.go | 19 ++++++ 5 files changed, 165 insertions(+), 38 deletions(-) diff --git a/drivers/hyperv/hyperv.go b/drivers/hyperv/hyperv.go index 329f55d..e15d328 100644 --- a/drivers/hyperv/hyperv.go +++ b/drivers/hyperv/hyperv.go @@ -26,6 +26,7 @@ type Driver struct { MacAddr string VLanID int DisableDynamicMemory bool + OS string } const ( @@ -35,6 +36,8 @@ const ( defaultVLanID = 0 defaultDisableDynamicMemory = false defaultSwitchID = "c08cb7b8-9b3c-408e-8e30-5e16a3aeb444" + defaultWindowsServerISOURL = "https://go.microsoft.com/fwlink/p/?LinkID=2195280&clcid=0x409&culture=en-us&country=US" + defaultVMDvdDriveISOURL = "https://github.com/vrapolinario/MinikubeWindowsContainers/raw/e6fbbed861c20099212f1e46e7a814b2b653c075/auto-install.iso" ) // NewDriver creates a new Hyper-v driver with default settings. @@ -186,17 +189,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" { + + if err := b2dutils.DownloadISO(b2dutils.GetImgCachePath(), "SERVER_EVAL_x64FRE_en-us.iso", defaultWindowsServerISOURL); err != nil { + return err + } + + err = b2dutils.DownloadISO(b2dutils.GetImgCachePath(), "auto-install.iso", defaultVMDvdDriveISOURL) + } else { + err = b2dutils.UpdateISOCache(d.Boot2DockerURL) + } + 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" { + if err := b2dutils.CopyWindowsIsoToMachineDir(d.MachineName); err != nil { + return err + } + } else { + if err := b2dutils.CopyIsoToMachineDir(d.Boot2DockerURL, d.MachineName); err != nil { + return err + } } log.Infof("Creating SSH key...") @@ -219,13 +240,27 @@ func (d *Driver) Create() error { return err } - if err := cmd("Hyper-V\\New-VM", - d.MachineName, - "-Path", fmt.Sprintf("'%s'", d.ResolveStorePath(".")), - "-SwitchName", quote(d.VSwitch), - "-MemoryStartupBytes", toMb(d.MemSize)); err != nil { - return err + if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { + if err := cmd("Hyper-V\\New-VM", + d.MachineName, + "-Path", fmt.Sprintf("'%s'", d.ResolveStorePath(".")), + "-SwitchName", quote(d.VSwitch), + "-NewVHDPath", quote("VHD.vhdx"), + "-NewVHDSizeBytes", toMb(d.DiskSize), + "-BootDevice", quote("VHD"), + "-MemoryStartupBytes", toMb(d.MemSize)); err != nil { + return err + } + } else { + if err := cmd("Hyper-V\\New-VM", + d.MachineName, + "-Path", fmt.Sprintf("'%s'", d.ResolveStorePath(".")), + "-SwitchName", quote(d.VSwitch), + "-MemoryStartupBytes", toMb(d.MemSize)); err != nil { + return err + } } + if d.DisableDynamicMemory { if err := cmd("Hyper-V\\Set-VMMemory", "-VMName", d.MachineName, @@ -237,7 +272,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 } } @@ -259,19 +295,35 @@ 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" { + if err := cmd("Hyper-V\\Set-VMDvdDrive", + "-VMName", d.MachineName, + "-Path", quote(d.ResolveStorePath("SERVER_EVAL_x64FRE_en-us.iso"))); err != nil { + return err + } + } 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-VMDvdDrive", + "-VMName", d.MachineName, + "-Path", quote(d.ResolveStorePath("auto-install.iso")), + "-ControllerNumber", quote("1"), + "-ControllerLocation", quote("1")); 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...") return d.Start() } @@ -507,3 +559,24 @@ func (d *Driver) generateDiskImage() (string, error) { return diskImage, nil } + +func Credential(d *Driver) error { + password := "Minikube@2024" + username := "Administrator" + + // PowerShell script to create a PSCredential object and execute Invoke-Command + psScript := ` + $SecurePassword = ConvertTo-SecureString -String "` + password + `" -AsPlainText -Force + $Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList "` + username + `", $SecurePassword + Invoke-Command -VMName "` + d.MachineName + `" -Credential $Credential -ScriptBlock {Get-Culture} + ` + + if err := cmd("powershell", "-Command", psScript); err != nil { + return err + } + + // log.Debugf the result of psScript + log.Debugf("psScript: %s", psScript) + + return nil +} diff --git a/libmachine/host/host.go b/libmachine/host/host.go index d36e187..c0efc70 100644 --- a/libmachine/host/host.go +++ b/libmachine/host/host.go @@ -44,6 +44,7 @@ type Host struct { HostOptions *Options Name string RawDriver []byte `json:"-"` + GuestOS string } type Options struct { diff --git a/libmachine/libmachine.go b/libmachine/libmachine.go index 5d0c8fb..66cebe4 100644 --- a/libmachine/libmachine.go +++ b/libmachine/libmachine.go @@ -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" @@ -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 @@ -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 @@ -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, @@ -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...") @@ -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...") @@ -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. @@ -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!") diff --git a/libmachine/mcnutils/b2d.go b/libmachine/mcnutils/b2d.go index dacc1fc..53c1790 100644 --- a/libmachine/mcnutils/b2d.go +++ b/libmachine/mcnutils/b2d.go @@ -22,11 +22,13 @@ import ( ) const ( - defaultURL = "https://api.github.com/repos/boot2docker/boot2docker/releases" - defaultISOFilename = "boot2docker.iso" - defaultVolumeIDOffset = int64(0x8028) - versionPrefix = "-v" - defaultVolumeIDLength = 32 + defaultURL = "https://api.github.com/repos/boot2docker/boot2docker/releases" + defaultISOFilename = "boot2docker.iso" + defaultWindowsISOFilename = "SERVER_EVAL_x64FRE_en-us.iso" + defaultWindowsAnswerFileISOFilename = "auto-install.iso" + defaultVolumeIDOffset = int64(0x8028) + versionPrefix = "-v" + defaultVolumeIDLength = 32 ) var ( @@ -356,6 +358,10 @@ func NewB2dUtils(storePath string) *B2dUtils { } } +func (b *B2dUtils) GetImgCachePath() string { + return b.imgCachePath +} + // DownloadISO downloads boot2docker ISO image for the given tag and save it at dest. func (b *B2dUtils) DownloadISO(dir, file, isoURL string) error { log.Infof("Downloading %s from %s...", b.path(), isoURL) @@ -466,6 +472,28 @@ func (b *B2dUtils) CopyIsoToMachineDir(isoURL, machineName string) error { return b.DownloadISO(machineDir, b.filename(), downloadURL) } +func (b *B2dUtils) CopyWindowsIsoToMachineDir(machineName string) error { + + machineDir := filepath.Join(b.storePath, "machines", machineName) + + windowsMachineIsoPath := filepath.Join(machineDir, "SERVER_EVAL_x64FRE_en-us.iso") + answerFilePath := filepath.Join(machineDir, "auto-install.iso") + + // cached location of the windows iso + windowsIsoPath := filepath.Join(b.imgCachePath, "SERVER_EVAL_x64FRE_en-us.iso") + // cached location of the answer file + answerFile := filepath.Join(b.imgCachePath, "auto-install.iso") + + log.Infof("Copying %s to %s...", windowsIsoPath, windowsMachineIsoPath) + err := CopyFile(windowsIsoPath, windowsMachineIsoPath) + if err != nil { + return err + } + log.Infof("Copying %s to %s...", answerFile, answerFilePath) + return CopyFile(answerFile, answerFilePath) + +} + // isLatest checks the latest release tag and // reports whether the local ISO cache is the latest version. // diff --git a/libmachine/mcnutils/utils.go b/libmachine/mcnutils/utils.go index a3992ed..0053e80 100644 --- a/libmachine/mcnutils/utils.go +++ b/libmachine/mcnutils/utils.go @@ -9,12 +9,31 @@ import ( "runtime" "strconv" "time" + + "github.com/docker/machine/libmachine/log" ) type MultiError struct { Errs []error } +var ConfigGuestOSUtil *GuestOSUtil + +type GuestOSUtil struct { + os string +} + +func SetGuestOSUtil(guestOS string) { + log.Debugf("==== we are being called") + ConfigGuestOSUtil = &GuestOSUtil{ + os: guestOS, + } +} + +func (g *GuestOSUtil) GetGuestOS() string { + return g.os +} + func (e MultiError) Error() string { aggregate := "" for _, err := range e.Errs { From e56d75ed52d722ec4d5910c9366fe432d969e87e Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Mon, 23 Sep 2024 12:22:51 +0100 Subject: [PATCH 02/16] logic to test windows installation --- drivers/hyperv/hyperv.go | 65 ++++++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/drivers/hyperv/hyperv.go b/drivers/hyperv/hyperv.go index e15d328..9ce2952 100644 --- a/drivers/hyperv/hyperv.go +++ b/drivers/hyperv/hyperv.go @@ -37,7 +37,7 @@ const ( defaultDisableDynamicMemory = false defaultSwitchID = "c08cb7b8-9b3c-408e-8e30-5e16a3aeb444" defaultWindowsServerISOURL = "https://go.microsoft.com/fwlink/p/?LinkID=2195280&clcid=0x409&culture=en-us&country=US" - defaultVMDvdDriveISOURL = "https://github.com/vrapolinario/MinikubeWindowsContainers/raw/e6fbbed861c20099212f1e46e7a814b2b653c075/auto-install.iso" + defaultVMDvdDriveISOURL = "https://github.com/vrapolinario/MinikubeWindowsContainers/raw/ed08391d61f4db9d3a670f5bfbb2cf8d4fe980f4/auto-install.iso" ) // NewDriver creates a new Hyper-v driver with default settings. @@ -382,6 +382,13 @@ func (d *Driver) chooseVirtualSwitch() (string, error) { func (d *Driver) waitForIP() (string, error) { log.Infof("Waiting for host to start...") + if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { + if err := TestWindowsInstallation(d); err != nil { + return "", err + } + + } + for { ip, _ := d.GetIP() if ip != "" { @@ -560,23 +567,55 @@ func (d *Driver) generateDiskImage() (string, error) { return diskImage, nil } -func Credential(d *Driver) error { +func GetCredentials() string { password := "Minikube@2024" username := "Administrator" - // PowerShell script to create a PSCredential object and execute Invoke-Command - psScript := ` - $SecurePassword = ConvertTo-SecureString -String "` + password + `" -AsPlainText -Force - $Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList "` + username + `", $SecurePassword - Invoke-Command -VMName "` + d.MachineName + `" -Credential $Credential -ScriptBlock {Get-Culture} - ` + // Construct the PowerShell script + psScript := fmt.Sprintf(`$SecurePassword = ConvertTo-SecureString -String "%s" -AsPlainText -Force; `+ + `$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList "%s", $SecurePassword;`, + password, username) - if err := cmd("powershell", "-Command", psScript); err != nil { - return err - } - - // log.Debugf the result of psScript + // Invoke-Command -VMName "` + d.MachineName + `" -Credential $Credential -ScriptBlock {Get-Culture} log.Debugf("psScript: %s", psScript) + return psScript +} + +func TestWindowsInstallation(d *Driver) error { + log.Debugf("=== TestWindowsInstallation === Checking if Windows is installed on %s", d.MachineName) + const retryInterval = 45 * time.Second + const timeout = 120 * time.Second + elapsedTime := 0 * time.Second + start := time.Now() + + for { + // Check if the timeout has been reached + if elapsedTime > timeout { + return fmt.Errorf("timeout reached while checking if Windows is installed on %s", d.MachineName) + } + + // PowerShell script to check if the OS is installed + credential := GetCredentials() + + psScript := fmt.Sprintf(`%s Invoke-Command -VMName %s -Credential $Credential -ScriptBlock {Get-WmiObject -Query 'SELECT * FROM Win32_OperatingSystem'}`, credential, d.MachineName) + stdout, err := cmdOut(psScript) + + // psScript := fmt.Sprintf(`%s Invoke-Command -VMName %s -Credential $Credential -ScriptBlock {Get-WmiObject -Query 'SELECT * FROM Win32_OperatingSystem'}`, credential, d.MachineName) + // fullScript := fmt.Sprintf(`powershell -NoProfile -NonInteractive -Command "& { %s }"`, psScript) + // stdout, err := cmdOut("powershell", "-NoProfile", "-NonInteractive", "-Command", fullScript) + + if err == nil { + log.Debugf("Windows is successfully installed on %s", d.MachineName) + log.Debugf("stdout: %s", stdout) + // parse the value of stdout to show the progress of the installation + break + } else { + log.Warnf("An error occurred while checking if Windows is installed on %s: %v", d.MachineName, err) + } + + time.Sleep(retryInterval) + elapsedTime = time.Since(start) + } return nil } From f6de88db8b052dd10d4a9443bff2723e8f61f612 Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Wed, 1 Jan 2025 11:31:18 +0000 Subject: [PATCH 03/16] logic for windows node initialization --- drivers/hyperv/hyperv.go | 99 +++------------------- libmachine/drivers/utils.go | 4 + libmachine/mcnutils/b2d.go | 91 +++++++++++++++----- libmachine/mcnutils/download_vhd.go | 125 ++++++++++++++++++++++++++++ libmachine/ssh/client.go | 6 +- 5 files changed, 215 insertions(+), 110 deletions(-) create mode 100644 libmachine/mcnutils/download_vhd.go diff --git a/drivers/hyperv/hyperv.go b/drivers/hyperv/hyperv.go index 9ce2952..86621aa 100644 --- a/drivers/hyperv/hyperv.go +++ b/drivers/hyperv/hyperv.go @@ -19,6 +19,7 @@ import ( type Driver struct { *drivers.BaseDriver Boot2DockerURL string + WindowsVHDUrl string VSwitch string DiskSize int MemSize int @@ -36,8 +37,7 @@ const ( defaultVLanID = 0 defaultDisableDynamicMemory = false defaultSwitchID = "c08cb7b8-9b3c-408e-8e30-5e16a3aeb444" - defaultWindowsServerISOURL = "https://go.microsoft.com/fwlink/p/?LinkID=2195280&clcid=0x409&culture=en-us&country=US" - defaultVMDvdDriveISOURL = "https://github.com/vrapolinario/MinikubeWindowsContainers/raw/ed08391d61f4db9d3a670f5bfbb2cf8d4fe980f4/auto-install.iso" + defaultServerImageUrl = "https://serverimagebuilder.blob.core.windows.net/windowsimagevhdx/GI-W11-001.vhdx" ) // NewDriver creates a new Hyper-v driver with default settings. @@ -46,6 +46,7 @@ func NewDriver(hostName, storePath string) *Driver { DiskSize: defaultDiskSize, MemSize: defaultMemory, CPU: defaultCPU, + WindowsVHDUrl: defaultServerImageUrl, DisableDynamicMemory: defaultDisableDynamicMemory, BaseDriver: &drivers.BaseDriver{ MachineName: hostName, @@ -145,7 +146,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) @@ -193,15 +194,10 @@ func (d *Driver) PreCreateCheck() error { // that a download failure will not leave a machine half created. b2dutils := mcnutils.NewB2dUtils(d.StorePath) - if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { - - if err := b2dutils.DownloadISO(b2dutils.GetImgCachePath(), "SERVER_EVAL_x64FRE_en-us.iso", defaultWindowsServerISOURL); err != nil { - return err - } - - err = b2dutils.DownloadISO(b2dutils.GetImgCachePath(), "auto-install.iso", defaultVMDvdDriveISOURL) - } else { + if mcnutils.ConfigGuestOSUtil.GetGuestOS() != "windows" { err = b2dutils.UpdateISOCache(d.Boot2DockerURL) + } else { + err = b2dutils.UpdateVHDCache(d.WindowsVHDUrl) } return err @@ -211,7 +207,7 @@ func (d *Driver) Create() error { b2dutils := mcnutils.NewB2dUtils(d.StorePath) if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { - if err := b2dutils.CopyWindowsIsoToMachineDir(d.MachineName); err != nil { + if err := b2dutils.CopyWindowsIsoToMachineDir(d.WindowsVHDUrl, d.MachineName); err != nil { return err } } else { @@ -243,18 +239,14 @@ func (d *Driver) Create() error { if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { if err := cmd("Hyper-V\\New-VM", d.MachineName, - "-Path", fmt.Sprintf("'%s'", d.ResolveStorePath(".")), "-SwitchName", quote(d.VSwitch), - "-NewVHDPath", quote("VHD.vhdx"), - "-NewVHDSizeBytes", toMb(d.DiskSize), - "-BootDevice", quote("VHD"), + "-Generation", quote("2"), "-MemoryStartupBytes", toMb(d.MemSize)); err != nil { return err } } else { if err := cmd("Hyper-V\\New-VM", d.MachineName, - "-Path", fmt.Sprintf("'%s'", d.ResolveStorePath(".")), "-SwitchName", quote(d.VSwitch), "-MemoryStartupBytes", toMb(d.MemSize)); err != nil { return err @@ -296,11 +288,7 @@ func (d *Driver) Create() error { } if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { - if err := cmd("Hyper-V\\Set-VMDvdDrive", - "-VMName", d.MachineName, - "-Path", quote(d.ResolveStorePath("SERVER_EVAL_x64FRE_en-us.iso"))); err != nil { - return err - } + // === Windows === } else { if err := cmd("Hyper-V\\Set-VMDvdDrive", "-VMName", d.MachineName, @@ -310,11 +298,10 @@ func (d *Driver) Create() error { } if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { - if err := cmd("Hyper-V\\Add-VMDvdDrive", + if err := cmd("Hyper-V\\Add-VMHardDiskDrive", "-VMName", d.MachineName, - "-Path", quote(d.ResolveStorePath("auto-install.iso")), - "-ControllerNumber", quote("1"), - "-ControllerLocation", quote("1")); err != nil { + "-Path", quote(d.ResolveStorePath("GI-W11-001.vhdx")), + "-ControllerType", "SCSI"); err != nil { return err } } else { @@ -382,13 +369,6 @@ func (d *Driver) chooseVirtualSwitch() (string, error) { func (d *Driver) waitForIP() (string, error) { log.Infof("Waiting for host to start...") - if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { - if err := TestWindowsInstallation(d); err != nil { - return "", err - } - - } - for { ip, _ := d.GetIP() if ip != "" { @@ -566,56 +546,3 @@ func (d *Driver) generateDiskImage() (string, error) { return diskImage, nil } - -func GetCredentials() string { - password := "Minikube@2024" - username := "Administrator" - - // Construct the PowerShell script - psScript := fmt.Sprintf(`$SecurePassword = ConvertTo-SecureString -String "%s" -AsPlainText -Force; `+ - `$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList "%s", $SecurePassword;`, - password, username) - - // Invoke-Command -VMName "` + d.MachineName + `" -Credential $Credential -ScriptBlock {Get-Culture} - log.Debugf("psScript: %s", psScript) - - return psScript -} - -func TestWindowsInstallation(d *Driver) error { - log.Debugf("=== TestWindowsInstallation === Checking if Windows is installed on %s", d.MachineName) - const retryInterval = 45 * time.Second - const timeout = 120 * time.Second - elapsedTime := 0 * time.Second - start := time.Now() - - for { - // Check if the timeout has been reached - if elapsedTime > timeout { - return fmt.Errorf("timeout reached while checking if Windows is installed on %s", d.MachineName) - } - - // PowerShell script to check if the OS is installed - credential := GetCredentials() - - psScript := fmt.Sprintf(`%s Invoke-Command -VMName %s -Credential $Credential -ScriptBlock {Get-WmiObject -Query 'SELECT * FROM Win32_OperatingSystem'}`, credential, d.MachineName) - stdout, err := cmdOut(psScript) - - // psScript := fmt.Sprintf(`%s Invoke-Command -VMName %s -Credential $Credential -ScriptBlock {Get-WmiObject -Query 'SELECT * FROM Win32_OperatingSystem'}`, credential, d.MachineName) - // fullScript := fmt.Sprintf(`powershell -NoProfile -NonInteractive -Command "& { %s }"`, psScript) - // stdout, err := cmdOut("powershell", "-NoProfile", "-NonInteractive", "-Command", fullScript) - - if err == nil { - log.Debugf("Windows is successfully installed on %s", d.MachineName) - log.Debugf("stdout: %s", stdout) - // parse the value of stdout to show the progress of the installation - break - } else { - log.Warnf("An error occurred while checking if Windows is installed on %s: %v", d.MachineName, err) - } - - time.Sleep(retryInterval) - elapsedTime = time.Since(start) - } - return nil -} diff --git a/libmachine/drivers/utils.go b/libmachine/drivers/utils.go index b023f16..6e41831 100644 --- a/libmachine/drivers/utils.go +++ b/libmachine/drivers/utils.go @@ -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 diff --git a/libmachine/mcnutils/b2d.go b/libmachine/mcnutils/b2d.go index 53c1790..accef80 100644 --- a/libmachine/mcnutils/b2d.go +++ b/libmachine/mcnutils/b2d.go @@ -22,13 +22,12 @@ import ( ) const ( - defaultURL = "https://api.github.com/repos/boot2docker/boot2docker/releases" - defaultISOFilename = "boot2docker.iso" - defaultWindowsISOFilename = "SERVER_EVAL_x64FRE_en-us.iso" - defaultWindowsAnswerFileISOFilename = "auto-install.iso" - defaultVolumeIDOffset = int64(0x8028) - versionPrefix = "-v" - defaultVolumeIDLength = 32 + defaultURL = "https://api.github.com/repos/boot2docker/boot2docker/releases" + defaultISOFilename = "boot2docker.iso" + defaultServerImageFilename = "GI-W11-001.vhdx" + defaultVolumeIDOffset = int64(0x8028) + versionPrefix = "-v" + defaultVolumeIDLength = 32 ) var ( @@ -196,6 +195,9 @@ Consider specifying another storage driver (e.g. 'overlay') using '--engine-stor func (*b2dReleaseGetter) download(dir, file, isoURL string) error { u, err := url.Parse(isoURL) + if err != nil { + return err + } var src io.ReadCloser if u.Scheme == "file" || u.Scheme == "" { @@ -263,6 +265,10 @@ type iso interface { path() string // exists reports whether the ISO exists. exists() bool + // pathVHD returns the path of the VHD. + pathVHD() string + // hasVHD returns whether the server VHD exists. + hasVHD() bool // version returns version information of the ISO. version() (string, error) } @@ -271,6 +277,8 @@ type iso interface { type b2dISO struct { // path of Boot2Docker ISO commonIsoPath string + // path of Windows Server VHD + commonVHDPath string // offset and length of ISO volume ID // cf. http://serverfault.com/questions/361474/is-there-a-way-to-change-a-iso-files-volume-id-from-the-command-line @@ -294,6 +302,22 @@ func (b *b2dISO) exists() bool { return !os.IsNotExist(err) } +func (b *b2dISO) pathVHD() string { + if b == nil { + return "" + } + return b.commonVHDPath +} + +func (b *b2dISO) hasVHD() bool { + if b == nil { + return false + } + + _, err := os.Stat(b.commonVHDPath) + return !os.IsNotExist(err) +} + // version scans the volume ID in b and returns its version tag. func (b *b2dISO) version() (string, error) { if b == nil { @@ -316,7 +340,7 @@ func (b *b2dISO) version() (string, error) { versionIndex := strings.Index(trimmedVersion, versionPrefix) if versionIndex == -1 { - return "", fmt.Errorf("Did not find prefix %q in version string", versionPrefix) + return "", fmt.Errorf("did not find prefix %q in version string", versionPrefix) } // Original magic file string looks similar to this: "Boot2Docker-v0.1.0 " @@ -350,6 +374,7 @@ func NewB2dUtils(storePath string) *B2dUtils { releaseGetter: &b2dReleaseGetter{isoFilename: defaultISOFilename}, iso: &b2dISO{ commonIsoPath: filepath.Join(imgCachePath, defaultISOFilename), + commonVHDPath: filepath.Join(imgCachePath, defaultServerImageFilename), volumeIDOffset: defaultVolumeIDOffset, volumeIDLength: defaultVolumeIDLength, }, @@ -368,6 +393,12 @@ func (b *B2dUtils) DownloadISO(dir, file, isoURL string) error { return b.download(dir, file, isoURL) } +// DownloadVHD downloads the Windows Server VHD image and saves it at dest. +func (b *B2dUtils) DownloadVHD(dir, file, vhdURL string) error { + log.Infof("Downloading %s from %s...", b.pathVHD(), vhdURL) + return b.download(dir, file, vhdURL) +} + type ReaderWithProgress struct { io.ReadCloser out io.Writer @@ -414,6 +445,28 @@ func (b *B2dUtils) DownloadISOFromURL(latestReleaseURL string) error { return b.DownloadISO(b.imgCachePath, b.filename(), latestReleaseURL) } +func (b *B2dUtils) UpdateVHDCache(defaultVHDUrl string) error { + // recreate the cache dir if it has been manually deleted + // this will already be taken care of by the UpdateISOCache method for linux ISO + + exists := b.hasVHD() + + if !exists { + log.Info("No default Windows Server VHD found locally, downloading the latest release...") + + filePath := filepath.Join(b.imgCachePath, defaultServerImageFilename) + + err := DownloadVHDX(defaultVHDUrl, filePath, 16) // Download using 16 parts + + if err != nil { + return fmt.Errorf("Error: %v", err) + } + log.Info("Windows Server VHD downloaded successfully") + } + + return nil +} + func (b *B2dUtils) UpdateISOCache(isoURL string) error { // recreate the cache dir if it has been manually deleted if _, err := os.Stat(b.imgCachePath); os.IsNotExist(err) { @@ -472,25 +525,21 @@ func (b *B2dUtils) CopyIsoToMachineDir(isoURL, machineName string) error { return b.DownloadISO(machineDir, b.filename(), downloadURL) } -func (b *B2dUtils) CopyWindowsIsoToMachineDir(machineName string) error { +func (b *B2dUtils) CopyWindowsIsoToMachineDir(VHDUrl, machineName string) error { + + if err := b.UpdateVHDCache(VHDUrl); err != nil { + return err + } machineDir := filepath.Join(b.storePath, "machines", machineName) - windowsMachineIsoPath := filepath.Join(machineDir, "SERVER_EVAL_x64FRE_en-us.iso") - answerFilePath := filepath.Join(machineDir, "auto-install.iso") + windowsMachineVHDPath := filepath.Join(machineDir, defaultServerImageFilename) // cached location of the windows iso - windowsIsoPath := filepath.Join(b.imgCachePath, "SERVER_EVAL_x64FRE_en-us.iso") - // cached location of the answer file - answerFile := filepath.Join(b.imgCachePath, "auto-install.iso") + windowsVHDPath := filepath.Join(b.imgCachePath, defaultServerImageFilename) - log.Infof("Copying %s to %s...", windowsIsoPath, windowsMachineIsoPath) - err := CopyFile(windowsIsoPath, windowsMachineIsoPath) - if err != nil { - return err - } - log.Infof("Copying %s to %s...", answerFile, answerFilePath) - return CopyFile(answerFile, answerFilePath) + log.Infof("Copying %s to %s...", windowsVHDPath, windowsMachineVHDPath) + return CopyFile(windowsVHDPath, windowsMachineVHDPath) } diff --git a/libmachine/mcnutils/download_vhd.go b/libmachine/mcnutils/download_vhd.go new file mode 100644 index 0000000..31f07c5 --- /dev/null +++ b/libmachine/mcnutils/download_vhd.go @@ -0,0 +1,125 @@ +package mcnutils + +import ( + "fmt" + "io" + "net/http" + "os" + "sync" + + "github.com/docker/machine/libmachine/log" +) + +type ProgressWriter struct { + Total int64 + Downloaded int64 + mu sync.Mutex +} + +func (pw *ProgressWriter) Write(p []byte) (int, error) { + n := len(p) + pw.mu.Lock() + pw.Downloaded += int64(n) + pw.PrintProgress() + pw.mu.Unlock() + return n, nil +} + +func (pw *ProgressWriter) PrintProgress() { + fmt.Printf("\rDownloading... %d/%d bytes complete", pw.Downloaded, pw.Total) +} + +func DownloadPart(url string, start, end int64, partFileName string, pw *ProgressWriter, wg *sync.WaitGroup) { + defer wg.Done() + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Infof("Error creating request: %v", err) + return + } + req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end)) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Infof("Error downloading part: %v", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusPartialContent { + log.Infof("Error: expected status 206 Partial Content, got %d", resp.StatusCode) + return + } + + partFile, err := os.Create(partFileName) + if err != nil { + log.Infof("Error creating part file: %v", err) + return + } + defer partFile.Close() + + buf := make([]byte, 32*1024) // 32 KB buffer + _, err = io.CopyBuffer(io.MultiWriter(partFile, pw), resp.Body, buf) + if err != nil { + log.Infof("Error saving part: %v", err) + return + } +} + +func DownloadVHDX(url string, filePath string, numParts int) error { + resp, err := http.Head(url) + if err != nil { + return fmt.Errorf("failed to get file info: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bad status: %s", resp.Status) + } + + totalSize := resp.ContentLength + partSize := totalSize / int64(numParts) + + pw := &ProgressWriter{Total: totalSize} + var wg sync.WaitGroup + + partFiles := make([]string, numParts) + for i := 0; i < numParts; i++ { + start := int64(i) * partSize + end := start + partSize - 1 + if i == numParts-1 { + end = totalSize - 1 + } + + partFileName := fmt.Sprintf("part-%d.tmp", i) + partFiles[i] = partFileName + + wg.Add(1) + go DownloadPart(url, start, end, partFileName, pw, &wg) + } + + wg.Wait() + + out, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer out.Close() + + for _, partFileName := range partFiles { + partFile, err := os.Open(partFileName) + if err != nil { + return fmt.Errorf("failed to open part file: %w", err) + } + + _, err = io.Copy(out, partFile) + partFile.Close() + if err != nil { + return fmt.Errorf("failed to merge part file: %w", err) + } + + os.Remove(partFileName) + } + + log.Infof("\nDownload complete") + return nil +} diff --git a/libmachine/ssh/client.go b/libmachine/ssh/client.go index 1f552fc..c7cb431 100644 --- a/libmachine/ssh/client.go +++ b/libmachine/ssh/client.go @@ -168,14 +168,14 @@ func (client *NativeClient) dialSuccess() bool { return true } -func (client *NativeClient) session(command string) (*ssh.Client, *ssh.Session, error) { +func (client *NativeClient) session(_ string) (*ssh.Client, *ssh.Session, error) { if err := mcnutils.WaitFor(client.dialSuccess); err != nil { - return nil, nil, fmt.Errorf("Error attempting SSH client dial: %s", err) + return nil, nil, fmt.Errorf("error attempting SSH client dial: %s", err) } conn, err := ssh.Dial("tcp", net.JoinHostPort(client.Hostname, strconv.Itoa(client.Port)), &client.Config) if err != nil { - return nil, nil, fmt.Errorf("Mysterious error dialing TCP for SSH (we already succeeded at least once) : %s", err) + return nil, nil, fmt.Errorf("mysterious error dialing TCP for SSH (we already succeeded at least once): %s", err) } session, err := conn.NewSession() From 90a9a549d23ff7aaba3a8b56fcc7200c9d5499f3 Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Tue, 7 Jan 2025 12:37:03 +0000 Subject: [PATCH 04/16] modified the location of the vhd remotely --- drivers/hyperv/hyperv.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/hyperv/hyperv.go b/drivers/hyperv/hyperv.go index 86621aa..847cd0b 100644 --- a/drivers/hyperv/hyperv.go +++ b/drivers/hyperv/hyperv.go @@ -37,7 +37,7 @@ const ( defaultVLanID = 0 defaultDisableDynamicMemory = false defaultSwitchID = "c08cb7b8-9b3c-408e-8e30-5e16a3aeb444" - defaultServerImageUrl = "https://serverimagebuilder.blob.core.windows.net/windowsimagevhdx/GI-W11-001.vhdx" + defaultServerImageUrl = "https://minikubevhdimagebuider.blob.core.windows.net/minikubevhdimage/GI-W11-001.vhdx" ) // NewDriver creates a new Hyper-v driver with default settings. From b12990f7f0c6bd53dc5bbede5bb79d22e1c806ac Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Thu, 30 Jan 2025 20:47:18 +0000 Subject: [PATCH 05/16] new windows server url --- drivers/hyperv/hyperv.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/hyperv/hyperv.go b/drivers/hyperv/hyperv.go index 847cd0b..78876c8 100644 --- a/drivers/hyperv/hyperv.go +++ b/drivers/hyperv/hyperv.go @@ -37,7 +37,7 @@ const ( defaultVLanID = 0 defaultDisableDynamicMemory = false defaultSwitchID = "c08cb7b8-9b3c-408e-8e30-5e16a3aeb444" - defaultServerImageUrl = "https://minikubevhdimagebuider.blob.core.windows.net/minikubevhdimage/GI-W11-001.vhdx" + defaultServerImageUrl = "https://minikubevhdimagebuider.blob.core.windows.net/minikubevhdimage/GI-W11-002.vhdx" ) // NewDriver creates a new Hyper-v driver with default settings. From eed9cde6dccd5bfea57c25f05847d1b76b8f995d Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Tue, 11 Feb 2025 02:21:41 +0000 Subject: [PATCH 06/16] changes to reflect ssh change --- drivers/hyperv/hyperv.go | 8 +++++--- libmachine/drivers/utils.go | 30 +++++++++++++++++++++++------- libmachine/mcnutils/b2d.go | 4 ++-- libmachine/mcnutils/utils.go | 5 ++++- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/drivers/hyperv/hyperv.go b/drivers/hyperv/hyperv.go index 78876c8..6af76c9 100644 --- a/drivers/hyperv/hyperv.go +++ b/drivers/hyperv/hyperv.go @@ -37,7 +37,7 @@ const ( defaultVLanID = 0 defaultDisableDynamicMemory = false defaultSwitchID = "c08cb7b8-9b3c-408e-8e30-5e16a3aeb444" - defaultServerImageUrl = "https://minikubevhdimagebuider.blob.core.windows.net/minikubevhdimage/GI-W11-002.vhdx" + defaultServerImageUrl = "https://minikubevhdimagebuider.blob.core.windows.net/minikubevhdimage/WIN-SER-2025.vhdx" ) // NewDriver creates a new Hyper-v driver with default settings. @@ -207,7 +207,8 @@ func (d *Driver) Create() error { b2dutils := mcnutils.NewB2dUtils(d.StorePath) if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { - if err := b2dutils.CopyWindowsIsoToMachineDir(d.WindowsVHDUrl, d.MachineName); err != nil { + d.SSHUser = "Administrator" + if err := b2dutils.CopyWindowsVHDToMachineDir(d.WindowsVHDUrl, d.MachineName); err != nil { return err } } else { @@ -217,6 +218,7 @@ func (d *Driver) Create() error { } log.Infof("Creating SSH key...") + if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil { return err } @@ -300,7 +302,7 @@ func (d *Driver) Create() error { if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { if err := cmd("Hyper-V\\Add-VMHardDiskDrive", "-VMName", d.MachineName, - "-Path", quote(d.ResolveStorePath("GI-W11-001.vhdx")), + "-Path", quote(d.ResolveStorePath("WIN-SER-2025.vhdx")), "-ControllerType", "SCSI"); err != nil { return err } diff --git a/libmachine/drivers/utils.go b/libmachine/drivers/utils.go index 6e41831..6531eaa 100644 --- a/libmachine/drivers/utils.go +++ b/libmachine/drivers/utils.go @@ -10,33 +10,49 @@ import ( func GetSSHClientFromDriver(d Driver) (ssh.Client, error) { address, err := d.GetSSHHostname() + log.Debugf("GetSSHHostname: %s", address) if err != nil { return nil, err } port, err := d.GetSSHPort() + log.Debugf("GetSSHPort: %d", port) if err != nil { return nil, err } var auth *ssh.Auth - if d.GetSSHKeyPath() == "" { - auth = &ssh.Auth{} + log.Debugf("GetSSHKeyPath: %s", d.GetSSHKeyPath()) + + if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { + if d.GetSSHKeyPath() == "" { + auth = &ssh.Auth{} + } else { + auth = &ssh.Auth{ + Keys: []string{d.GetSSHKeyPath()}, + Passwords: []string{"password"}, // Add a password if needed + } + } } else { - auth = &ssh.Auth{ - Keys: []string{d.GetSSHKeyPath()}, + if d.GetSSHKeyPath() == "" { + auth = &ssh.Auth{} + } else { + auth = &ssh.Auth{ + Keys: []string{d.GetSSHKeyPath()}, + } } } + log.Debugf("GetSSHUsername: %s", d.GetSSHUsername()) client, err := ssh.NewClient(d.GetSSHUsername(), address, port, auth) return client, err } func RunSSHCommandFromDriver(d Driver, command string) (string, error) { - if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { - return "", fmt.Errorf("SSH commands are not supported on Windows") - } + // if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { + // return "", fmt.Errorf("SSH commands are not supported on Windows") + // } client, err := GetSSHClientFromDriver(d) if err != nil { diff --git a/libmachine/mcnutils/b2d.go b/libmachine/mcnutils/b2d.go index accef80..d49d1f6 100644 --- a/libmachine/mcnutils/b2d.go +++ b/libmachine/mcnutils/b2d.go @@ -24,7 +24,7 @@ import ( const ( defaultURL = "https://api.github.com/repos/boot2docker/boot2docker/releases" defaultISOFilename = "boot2docker.iso" - defaultServerImageFilename = "GI-W11-001.vhdx" + defaultServerImageFilename = "WIN-SER-2025.vhdx" defaultVolumeIDOffset = int64(0x8028) versionPrefix = "-v" defaultVolumeIDLength = 32 @@ -525,7 +525,7 @@ func (b *B2dUtils) CopyIsoToMachineDir(isoURL, machineName string) error { return b.DownloadISO(machineDir, b.filename(), downloadURL) } -func (b *B2dUtils) CopyWindowsIsoToMachineDir(VHDUrl, machineName string) error { +func (b *B2dUtils) CopyWindowsVHDToMachineDir(VHDUrl, machineName string) error { if err := b.UpdateVHDCache(VHDUrl); err != nil { return err diff --git a/libmachine/mcnutils/utils.go b/libmachine/mcnutils/utils.go index 0053e80..381e29a 100644 --- a/libmachine/mcnutils/utils.go +++ b/libmachine/mcnutils/utils.go @@ -24,13 +24,16 @@ type GuestOSUtil struct { } func SetGuestOSUtil(guestOS string) { - log.Debugf("==== we are being called") ConfigGuestOSUtil = &GuestOSUtil{ os: guestOS, } } func (g *GuestOSUtil) GetGuestOS() string { + if g == nil { + log.Debugf("GuestOSUtil is not initialized") + return "unknown" + } return g.os } From 89ef3513a439cb2c4b2c905b4bec4062fcd9b41d Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Wed, 19 Feb 2025 12:04:49 +0000 Subject: [PATCH 07/16] fixed ssh authnetication on windows guest --- drivers/hyperv/hyperv.go | 19 +++++++++++- libmachine/drivers/utils.go | 24 +++----------- libmachine/mcnutils/b2d.go | 62 +++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 21 deletions(-) diff --git a/drivers/hyperv/hyperv.go b/drivers/hyperv/hyperv.go index 6af76c9..89e9c8a 100644 --- a/drivers/hyperv/hyperv.go +++ b/drivers/hyperv/hyperv.go @@ -233,7 +233,19 @@ 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 := d.makeDiskImage(); 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 } @@ -497,6 +509,11 @@ func (d *Driver) publicSSHKeyPath() string { return d.GetSSHKeyPath() + ".pub" } +// makeDiskImage bundles ssh key in a vhd +func (d *Driver) makeDiskImage() error { + return mcnutils.WriteSSHKeyToVHDX(d.ResolveStorePath(mcnutils.GetDefaultServerImageFilename()), d.publicSSHKeyPath()) +} + // generateDiskImage creates a small fixed vhd, put the tar in, convert to dynamic, then resize func (d *Driver) generateDiskImage() (string, error) { diskImage := d.ResolveStorePath("disk.vhd") diff --git a/libmachine/drivers/utils.go b/libmachine/drivers/utils.go index 6531eaa..3e0fa01 100644 --- a/libmachine/drivers/utils.go +++ b/libmachine/drivers/utils.go @@ -10,40 +10,24 @@ import ( func GetSSHClientFromDriver(d Driver) (ssh.Client, error) { address, err := d.GetSSHHostname() - log.Debugf("GetSSHHostname: %s", address) if err != nil { return nil, err } port, err := d.GetSSHPort() - log.Debugf("GetSSHPort: %d", port) if err != nil { return nil, err } var auth *ssh.Auth - log.Debugf("GetSSHKeyPath: %s", d.GetSSHKeyPath()) - - if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { - if d.GetSSHKeyPath() == "" { - auth = &ssh.Auth{} - } else { - auth = &ssh.Auth{ - Keys: []string{d.GetSSHKeyPath()}, - Passwords: []string{"password"}, // Add a password if needed - } - } + if d.GetSSHKeyPath() == "" { + auth = &ssh.Auth{} } else { - if d.GetSSHKeyPath() == "" { - auth = &ssh.Auth{} - } else { - auth = &ssh.Auth{ - Keys: []string{d.GetSSHKeyPath()}, - } + auth = &ssh.Auth{ + Keys: []string{d.GetSSHKeyPath()}, } } - log.Debugf("GetSSHUsername: %s", d.GetSSHUsername()) client, err := ssh.NewClient(d.GetSSHUsername(), address, port, auth) return client, err diff --git a/libmachine/mcnutils/b2d.go b/libmachine/mcnutils/b2d.go index d49d1f6..65db2f9 100644 --- a/libmachine/mcnutils/b2d.go +++ b/libmachine/mcnutils/b2d.go @@ -12,6 +12,7 @@ import ( "net/http" "net/url" "os" + "os/exec" "path/filepath" "regexp" "runtime" @@ -622,3 +623,64 @@ func MakeDiskImage(publicSSHKeyPath string) (*bytes.Buffer, error) { return buf, nil } + +// function to return defaultServerImageFilename +func GetDefaultServerImageFilename() string { + return defaultServerImageFilename +} + +func WriteSSHKeyToVHDX(vhdxPath, publicSSHKeyPath string) error { + mountDir := "D:\\" + 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) + } + + if err := mountVHDX(vhdxPath); err != nil { + return fmt.Errorf("failed to mount VHDX: %w", err) + } + + 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) + } + + cmd := exec.Command("icacls.exe", adminAuthKeys, "/inheritance:r", "/grant", "Administrators:F", "/grant", "SYSTEM:F") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to set permissions: %w", err) + } + + if err := unmountVHDX(vhdxPath); err != nil { + return fmt.Errorf("failed to unmount VHDX: %w", err) + } + + return nil +} + +func mountVHDX(vhdxPath string) error { + cmd := exec.Command("powershell", "-Command", fmt.Sprintf( + "Mount-DiskImage -ImagePath '%s' -StorageType VHDX -PassThru | Get-Disk | Set-Disk -IsReadOnly $false", + vhdxPath, + )) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func unmountVHDX(vhdxPath string) error { + cmd := exec.Command("powershell", "-Command", fmt.Sprintf( + "Dismount-DiskImage -ImagePath '%s'", + vhdxPath, + )) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} From 4c949c1973c1ab2f3e0f838a3beeee8595de5309 Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Thu, 20 Feb 2025 22:56:15 +0000 Subject: [PATCH 08/16] function refactor --- drivers/hyperv/hyperv.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/drivers/hyperv/hyperv.go b/drivers/hyperv/hyperv.go index 89e9c8a..4ffbf18 100644 --- a/drivers/hyperv/hyperv.go +++ b/drivers/hyperv/hyperv.go @@ -235,7 +235,7 @@ func (d *Driver) Create() error { if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { log.Infof("Adding SSH key to the VHDX...") - if err := d.makeDiskImage(); err != nil { + if err := mcnutils.WriteSSHKeyToVHDX(d.ResolveStorePath(mcnutils.GetDefaultServerImageFilename()), d.publicSSHKeyPath()); err != nil { log.Errorf("Error creating disk image: %s", err) return err } @@ -509,11 +509,6 @@ func (d *Driver) publicSSHKeyPath() string { return d.GetSSHKeyPath() + ".pub" } -// makeDiskImage bundles ssh key in a vhd -func (d *Driver) makeDiskImage() error { - return mcnutils.WriteSSHKeyToVHDX(d.ResolveStorePath(mcnutils.GetDefaultServerImageFilename()), d.publicSSHKeyPath()) -} - // generateDiskImage creates a small fixed vhd, put the tar in, convert to dynamic, then resize func (d *Driver) generateDiskImage() (string, error) { diskImage := d.ResolveStorePath("disk.vhd") From 84125a62a630c9fed8e8e01ff39408b66a57530e Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Wed, 5 Mar 2025 08:47:44 +0000 Subject: [PATCH 09/16] refactored the mount logic --- drivers/hyperv/hyperv.go | 79 ++++++++++++++++++++++++++++++-------- libmachine/mcnutils/b2d.go | 62 ------------------------------ 2 files changed, 62 insertions(+), 79 deletions(-) diff --git a/drivers/hyperv/hyperv.go b/drivers/hyperv/hyperv.go index 4ffbf18..5a4c05b 100644 --- a/drivers/hyperv/hyperv.go +++ b/drivers/hyperv/hyperv.go @@ -3,6 +3,7 @@ package hyperv import ( "encoding/json" "fmt" + "io/ioutil" "net" "os" "strings" @@ -37,7 +38,8 @@ const ( defaultVLanID = 0 defaultDisableDynamicMemory = false defaultSwitchID = "c08cb7b8-9b3c-408e-8e30-5e16a3aeb444" - defaultServerImageUrl = "https://minikubevhdimagebuider.blob.core.windows.net/minikubevhdimage/WIN-SER-2025.vhdx" + defaultWindowsServerVHD = "https://minikubevhdimagebuider.blob.core.windows.net/minikubevhdimage/WIN-SER-2025.vhdx" + defaultServerImageFilename = "WIN-SER-2025.vhdx" ) // NewDriver creates a new Hyper-v driver with default settings. @@ -46,7 +48,7 @@ func NewDriver(hostName, storePath string) *Driver { DiskSize: defaultDiskSize, MemSize: defaultMemory, CPU: defaultCPU, - WindowsVHDUrl: defaultServerImageUrl, + WindowsVHDUrl: defaultWindowsServerVHD, DisableDynamicMemory: defaultDisableDynamicMemory, BaseDriver: &drivers.BaseDriver{ MachineName: hostName, @@ -235,7 +237,7 @@ func (d *Driver) Create() error { if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { log.Infof("Adding SSH key to the VHDX...") - if err := mcnutils.WriteSSHKeyToVHDX(d.ResolveStorePath(mcnutils.GetDefaultServerImageFilename()), d.publicSSHKeyPath()); err != nil { + if err := writeSSHKeyToVHDX(d.ResolveStorePath(defaultServerImageFilename), d.publicSSHKeyPath()); err != nil { log.Errorf("Error creating disk image: %s", err) return err } @@ -250,21 +252,17 @@ func (d *Driver) Create() error { return err } + vmGeneration := "1" if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { - if err := cmd("Hyper-V\\New-VM", - d.MachineName, - "-SwitchName", quote(d.VSwitch), - "-Generation", quote("2"), - "-MemoryStartupBytes", toMb(d.MemSize)); err != nil { - return err - } - } else { - if err := cmd("Hyper-V\\New-VM", - d.MachineName, - "-SwitchName", quote(d.VSwitch), - "-MemoryStartupBytes", toMb(d.MemSize)); err != nil { - return err - } + vmGeneration = "2" + } + + if err := cmd("Hyper-V\\New-VM", + d.MachineName, + "-SwitchName", quote(d.VSwitch), + "-Generation", quote(vmGeneration), + "-MemoryStartupBytes", toMb(d.MemSize)); err != nil { + return err } if d.DisableDynamicMemory { @@ -560,3 +558,50 @@ func (d *Driver) generateDiskImage() (string, error) { return diskImage, nil } + +func writeSSHKeyToVHDX(vhdxPath, publicSSHKeyPath string) error { + mountDir := "E:\\" + 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") + if err != nil { + return fmt.Errorf("failed to set permissions: %w", err) + } + + return nil +} diff --git a/libmachine/mcnutils/b2d.go b/libmachine/mcnutils/b2d.go index 65db2f9..d49d1f6 100644 --- a/libmachine/mcnutils/b2d.go +++ b/libmachine/mcnutils/b2d.go @@ -12,7 +12,6 @@ import ( "net/http" "net/url" "os" - "os/exec" "path/filepath" "regexp" "runtime" @@ -623,64 +622,3 @@ func MakeDiskImage(publicSSHKeyPath string) (*bytes.Buffer, error) { return buf, nil } - -// function to return defaultServerImageFilename -func GetDefaultServerImageFilename() string { - return defaultServerImageFilename -} - -func WriteSSHKeyToVHDX(vhdxPath, publicSSHKeyPath string) error { - mountDir := "D:\\" - 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) - } - - if err := mountVHDX(vhdxPath); err != nil { - return fmt.Errorf("failed to mount VHDX: %w", err) - } - - 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) - } - - cmd := exec.Command("icacls.exe", adminAuthKeys, "/inheritance:r", "/grant", "Administrators:F", "/grant", "SYSTEM:F") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to set permissions: %w", err) - } - - if err := unmountVHDX(vhdxPath); err != nil { - return fmt.Errorf("failed to unmount VHDX: %w", err) - } - - return nil -} - -func mountVHDX(vhdxPath string) error { - cmd := exec.Command("powershell", "-Command", fmt.Sprintf( - "Mount-DiskImage -ImagePath '%s' -StorageType VHDX -PassThru | Get-Disk | Set-Disk -IsReadOnly $false", - vhdxPath, - )) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} - -func unmountVHDX(vhdxPath string) error { - cmd := exec.Command("powershell", "-Command", fmt.Sprintf( - "Dismount-DiskImage -ImagePath '%s'", - vhdxPath, - )) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} From 963f9b46864baa3fa699fa9f4a0f4c62d10002ed Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Wed, 12 Mar 2025 01:51:17 +0000 Subject: [PATCH 10/16] dynamically getting the drive letter --- drivers/hyperv/hyperv.go | 44 ++++++++++++++++++++++-------------- drivers/hyperv/powershell.go | 2 +- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/drivers/hyperv/hyperv.go b/drivers/hyperv/hyperv.go index 5a4c05b..e85f974 100644 --- a/drivers/hyperv/hyperv.go +++ b/drivers/hyperv/hyperv.go @@ -2,10 +2,13 @@ package hyperv import ( "encoding/json" + "errors" "fmt" "io/ioutil" "net" "os" + "path/filepath" + "regexp" "strings" "time" @@ -558,34 +561,41 @@ func (d *Driver) generateDiskImage() (string, error) { return diskImage, nil } - func writeSSHKeyToVHDX(vhdxPath, publicSSHKeyPath string) error { - mountDir := "E:\\" - sshDir := mountDir + "ProgramData\\ssh\\" - adminAuthKeys := sshDir + "administrators_authorized_keys" - - pubKey, err := ioutil.ReadFile(publicSSHKeyPath) + 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 read public SSH key: %w", err) + return fmt.Errorf("failed to mount VHDX and retrieve mount directory: %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) + regex := regexp.MustCompile(`\s+|\r|\n`) + driveLetter := regex.ReplaceAllString(output, "") + + if driveLetter == "" { + return errors.New("no drive letter assigned to VHDX") } + + mountDir := strings.TrimSpace(driveLetter) + ":\\" + defer func() { - // Ensure unmounting even in case of failure - unmountErr := cmd("Dismount-DiskImage", "-ImagePath", quote(vhdxPath)) - if unmountErr != nil { + if unmountErr := cmd("Dismount-DiskImage", "-ImagePath", quote(vhdxPath)); unmountErr != nil { log.Warnf("Failed to unmount VHDX: %v", unmountErr) } }() - // Wait for mount stability - time.Sleep(2 * time.Second) + sshDir := filepath.Join(mountDir, "ProgramData", "ssh") + adminAuthKeys := filepath.Join(sshDir, "administrators_authorized_keys") + + pubKey, err := ioutil.ReadFile(publicSSHKeyPath) + if err != nil { + return fmt.Errorf("failed to read public SSH key: %w", err) + } - // Ensure mount directory exists if _, err := os.Stat(mountDir); os.IsNotExist(err) { return fmt.Errorf("mount point %s does not exist", mountDir) } diff --git a/drivers/hyperv/powershell.go b/drivers/hyperv/powershell.go index 3447b07..2407f20 100644 --- a/drivers/hyperv/powershell.go +++ b/drivers/hyperv/powershell.go @@ -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 { From 42d6a737c60b4c64984a56818fdd13d21f1e8e77 Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Wed, 12 Mar 2025 02:02:30 +0000 Subject: [PATCH 11/16] cleanup, commented code removal --- libmachine/drivers/utils.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libmachine/drivers/utils.go b/libmachine/drivers/utils.go index 3e0fa01..b023f16 100644 --- a/libmachine/drivers/utils.go +++ b/libmachine/drivers/utils.go @@ -34,10 +34,6 @@ 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 From 896dc6558266b9ab689c45324f33b874004e7f7b Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Sun, 27 Apr 2025 02:04:55 +0100 Subject: [PATCH 12/16] added retries to the download logic --- drivers/hyperv/hyperv.go | 3 +- drivers/hyperv/powershell.go | 2 +- libmachine/examples/main.go | 4 +- libmachine/libmachinetest/fake_api.go | 2 +- libmachine/mcnutils/b2d.go | 5 +- libmachine/mcnutils/b2d_test.go | 18 ++++-- libmachine/mcnutils/download_vhd.go | 92 +++++++++++++++++---------- 7 files changed, 84 insertions(+), 42 deletions(-) diff --git a/drivers/hyperv/hyperv.go b/drivers/hyperv/hyperv.go index e85f974..420712c 100644 --- a/drivers/hyperv/hyperv.go +++ b/drivers/hyperv/hyperv.go @@ -577,10 +577,11 @@ func writeSSHKeyToVHDX(vhdxPath, publicSSHKeyPath string) error { 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) + ":\\" + mountDir := strings.TrimSpace(driveLetter) + ":" + string(os.PathSeparator) defer func() { if unmountErr := cmd("Dismount-DiskImage", "-ImagePath", quote(vhdxPath)); unmountErr != nil { diff --git a/drivers/hyperv/powershell.go b/drivers/hyperv/powershell.go index 2407f20..be15cdd 100644 --- a/drivers/hyperv/powershell.go +++ b/drivers/hyperv/powershell.go @@ -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 } diff --git a/libmachine/examples/main.go b/libmachine/examples/main.go index 7decc83..f7e55de 100644 --- a/libmachine/examples/main.go +++ b/libmachine/examples/main.go @@ -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 @@ -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 diff --git a/libmachine/libmachinetest/fake_api.go b/libmachine/libmachinetest/fake_api.go index 4566257..3555f5c 100644 --- a/libmachine/libmachinetest/fake_api.go +++ b/libmachine/libmachinetest/fake_api.go @@ -20,7 +20,7 @@ func (api *FakeAPI) Close() error { return nil } -func (api *FakeAPI) NewHost(driverName string, rawDriver []byte) (*host.Host, error) { +func (api *FakeAPI) NewHost(driverName string, guestOS string, rawDriver []byte) (*host.Host, error) { return nil, nil } diff --git a/libmachine/mcnutils/b2d.go b/libmachine/mcnutils/b2d.go index d49d1f6..771b6fd 100644 --- a/libmachine/mcnutils/b2d.go +++ b/libmachine/mcnutils/b2d.go @@ -456,7 +456,10 @@ func (b *B2dUtils) UpdateVHDCache(defaultVHDUrl string) error { filePath := filepath.Join(b.imgCachePath, defaultServerImageFilename) - err := DownloadVHDX(defaultVHDUrl, filePath, 16) // Download using 16 parts + fmt.Printf("\n") + fmt.Printf(" * Downloading and caching Windows Server VHD image...\n") + fmt.Printf(" * This may take a while...\n") + err := DownloadVHDX(defaultVHDUrl, filePath, 16, 1) // Download using 16 parts if err != nil { return fmt.Errorf("Error: %v", err) diff --git a/libmachine/mcnutils/b2d_test.go b/libmachine/mcnutils/b2d_test.go index cf1cfad..c243229 100644 --- a/libmachine/mcnutils/b2d_test.go +++ b/libmachine/mcnutils/b2d_test.go @@ -207,10 +207,12 @@ func (m *mockReleaseGetter) download(dir, file, isoURL string) error { } type mockISO struct { - isopath string - exist bool - ver string - verCh <-chan string + isopath string + exist bool + ver string + vhdpath string + vhdexist bool + verCh <-chan string } func (m *mockISO) path() string { @@ -221,6 +223,14 @@ func (m *mockISO) exists() bool { return m.exist } +func (m *mockISO) pathVHD() string { + return m.vhdpath +} + +func (m *mockISO) hasVHD() bool { + return m.vhdexist +} + func (m *mockISO) version() (string, error) { select { // receive version of a downloaded iso diff --git a/libmachine/mcnutils/download_vhd.go b/libmachine/mcnutils/download_vhd.go index 31f07c5..bf08cb9 100644 --- a/libmachine/mcnutils/download_vhd.go +++ b/libmachine/mcnutils/download_vhd.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "sync" + "time" "github.com/docker/machine/libmachine/log" ) @@ -26,47 +27,61 @@ func (pw *ProgressWriter) Write(p []byte) (int, error) { } func (pw *ProgressWriter) PrintProgress() { - fmt.Printf("\rDownloading... %d/%d bytes complete", pw.Downloaded, pw.Total) + fmt.Printf("\r > %s...: %d / %d bytes complete \t", defaultServerImageFilename, pw.Downloaded, pw.Total) } -func DownloadPart(url string, start, end int64, partFileName string, pw *ProgressWriter, wg *sync.WaitGroup) { +func DownloadPart(url string, start, end int64, partFileName string, pw *ProgressWriter, wg *sync.WaitGroup, retryLimit int) error { defer wg.Done() - req, err := http.NewRequest("GET", url, nil) - if err != nil { - log.Infof("Error creating request: %v", err) - return - } - req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end)) + var resp *http.Response - resp, err := http.DefaultClient.Do(req) - if err != nil { - log.Infof("Error downloading part: %v", err) - return - } - defer resp.Body.Close() + // Retry loop for downloading a part + for retries := 0; retries <= retryLimit; retries++ { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Errorf("Error creating request: %v", err) + return err + } + req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end)) - if resp.StatusCode != http.StatusPartialContent { - log.Infof("Error: expected status 206 Partial Content, got %d", resp.StatusCode) - return - } + resp, err = http.DefaultClient.Do(req) + if err != nil { + log.Errorf("Error downloading part: %v", err) + // Retry after waiting a bit + time.Sleep(time.Duration(2< 0 { + return fmt.Errorf("download failed for the following parts: %v", downloadErrors) + } + + // Proceed with merging the parts as before out, err := os.Create(filePath) if err != nil { return fmt.Errorf("failed to create file: %w", err) @@ -120,6 +148,6 @@ func DownloadVHDX(url string, filePath string, numParts int) error { os.Remove(partFileName) } - log.Infof("\nDownload complete") + log.Infof("\n\r\t> Download complete") return nil } From 4ccf98c399a64425d2e70afaa62af494d8272ed4 Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Mon, 16 Jun 2025 12:03:26 +0100 Subject: [PATCH 13/16] user personalized server vhd logic --- drivers/hyperv/hyperv.go | 21 ++- libmachine/host/host.go | 8 +- libmachine/libmachine.go | 8 +- libmachine/mcnutils/b2d.go | 2 +- libmachine/mcnutils/download_vhd.go | 209 +++++++++++++++++++--------- libmachine/mcnutils/utils.go | 52 +++++-- 6 files changed, 211 insertions(+), 89 deletions(-) diff --git a/drivers/hyperv/hyperv.go b/drivers/hyperv/hyperv.go index 420712c..70b3e88 100644 --- a/drivers/hyperv/hyperv.go +++ b/drivers/hyperv/hyperv.go @@ -41,8 +41,7 @@ const ( defaultVLanID = 0 defaultDisableDynamicMemory = false defaultSwitchID = "c08cb7b8-9b3c-408e-8e30-5e16a3aeb444" - defaultWindowsServerVHD = "https://minikubevhdimagebuider.blob.core.windows.net/minikubevhdimage/WIN-SER-2025.vhdx" - defaultServerImageFilename = "WIN-SER-2025.vhdx" + defaultServerImageFilename = "hybrid-minikube-windows-server.vhdx" ) // NewDriver creates a new Hyper-v driver with default settings. @@ -51,7 +50,7 @@ func NewDriver(hostName, storePath string) *Driver { DiskSize: defaultDiskSize, MemSize: defaultMemory, CPU: defaultCPU, - WindowsVHDUrl: defaultWindowsServerVHD, + WindowsVHDUrl: mcnutils.ConfigGuest.GetVHDUrl(), DisableDynamicMemory: defaultDisableDynamicMemory, BaseDriver: &drivers.BaseDriver{ MachineName: hostName, @@ -199,7 +198,7 @@ func (d *Driver) PreCreateCheck() error { // that a download failure will not leave a machine half created. b2dutils := mcnutils.NewB2dUtils(d.StorePath) - if mcnutils.ConfigGuestOSUtil.GetGuestOS() != "windows" { + if mcnutils.ConfigGuest.GetGuestOS() != "windows" { err = b2dutils.UpdateISOCache(d.Boot2DockerURL) } else { err = b2dutils.UpdateVHDCache(d.WindowsVHDUrl) @@ -211,7 +210,7 @@ func (d *Driver) PreCreateCheck() error { func (d *Driver) Create() error { b2dutils := mcnutils.NewB2dUtils(d.StorePath) - if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { + if mcnutils.ConfigGuest.GetGuestOS() == "windows" { d.SSHUser = "Administrator" if err := b2dutils.CopyWindowsVHDToMachineDir(d.WindowsVHDUrl, d.MachineName); err != nil { return err @@ -238,7 +237,7 @@ func (d *Driver) Create() error { } log.Infof("Using switch %q", d.VSwitch) - if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { + 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) @@ -248,7 +247,7 @@ func (d *Driver) Create() error { var diskImage string var err error - if mcnutils.ConfigGuestOSUtil.GetGuestOS() != "windows" { + if mcnutils.ConfigGuest.GetGuestOS() != "windows" { diskImage, err = d.generateDiskImage() } if err != nil { @@ -256,7 +255,7 @@ func (d *Driver) Create() error { } vmGeneration := "1" - if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { + if mcnutils.ConfigGuest.GetGuestOS() == "windows" { vmGeneration = "2" } @@ -302,7 +301,7 @@ func (d *Driver) Create() error { } } - if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { + if mcnutils.ConfigGuest.GetGuestOS() == "windows" { // === Windows === } else { if err := cmd("Hyper-V\\Set-VMDvdDrive", @@ -312,10 +311,10 @@ func (d *Driver) Create() error { } } - if mcnutils.ConfigGuestOSUtil.GetGuestOS() == "windows" { + if mcnutils.ConfigGuest.GetGuestOS() == "windows" { if err := cmd("Hyper-V\\Add-VMHardDiskDrive", "-VMName", d.MachineName, - "-Path", quote(d.ResolveStorePath("WIN-SER-2025.vhdx")), + "-Path", quote(d.ResolveStorePath("hybrid-minikube-windows-server.vhdx")), "-ControllerType", "SCSI"); err != nil { return err } diff --git a/libmachine/host/host.go b/libmachine/host/host.go index c0efc70..ac28abc 100644 --- a/libmachine/host/host.go +++ b/libmachine/host/host.go @@ -44,7 +44,7 @@ type Host struct { HostOptions *Options Name string RawDriver []byte `json:"-"` - GuestOS string + Guest Guest } type Options struct { @@ -62,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) } diff --git a/libmachine/libmachine.go b/libmachine/libmachine.go index 66cebe4..cc138f7 100644 --- a/libmachine/libmachine.go +++ b/libmachine/libmachine.go @@ -28,7 +28,7 @@ import ( type API interface { io.Closer - NewHost(driverName string, guestOS string, rawDriver []byte) (*host.Host, error) + NewHost(driverName string, guest host.Guest, rawDriver []byte) (*host.Host, error) DefineGuest(h *host.Host) Create(h *host.Host) error persist.Store @@ -54,7 +54,7 @@ func NewClient(storePath, certsDir string) *Client { } } -func (api *Client) NewHost(driverName string, guestOS string, rawDriver []byte) (*host.Host, error) { +func (api *Client) NewHost(driverName string, guest host.Guest, rawDriver []byte) (*host.Host, error) { driver, err := api.clientDriverFactory.NewRPCClientDriver(driverName, rawDriver) if err != nil { return nil, err @@ -65,7 +65,7 @@ func (api *Client) NewHost(driverName string, guestOS string, rawDriver []byte) Name: driver.GetMachineName(), Driver: driver, DriverName: driver.DriverName(), - GuestOS: guestOS, + Guest: guest, HostOptions: &host.Options{ AuthOptions: &auth.Options{ CertDir: api.certsDir, @@ -116,7 +116,7 @@ func (api *Client) Load(name string) (*host.Host, error) { } func (api *Client) DefineGuest(h *host.Host) { - mcnutils.SetGuestOSUtil(h.GuestOS) + mcnutils.SetGuestUtil(h.Guest.Name, h.Guest.URL) } // Create is the wrapper method which covers all of the boilerplate around diff --git a/libmachine/mcnutils/b2d.go b/libmachine/mcnutils/b2d.go index 771b6fd..409bc6b 100644 --- a/libmachine/mcnutils/b2d.go +++ b/libmachine/mcnutils/b2d.go @@ -24,7 +24,7 @@ import ( const ( defaultURL = "https://api.github.com/repos/boot2docker/boot2docker/releases" defaultISOFilename = "boot2docker.iso" - defaultServerImageFilename = "WIN-SER-2025.vhdx" + defaultServerImageFilename = "hybrid-minikube-windows-server.vhdx" defaultVolumeIDOffset = int64(0x8028) versionPrefix = "-v" defaultVolumeIDLength = 32 diff --git a/libmachine/mcnutils/download_vhd.go b/libmachine/mcnutils/download_vhd.go index bf08cb9..d7b6225 100644 --- a/libmachine/mcnutils/download_vhd.go +++ b/libmachine/mcnutils/download_vhd.go @@ -4,7 +4,10 @@ import ( "fmt" "io" "net/http" + "net/url" "os" + "path/filepath" + "strings" "sync" "time" @@ -15,90 +18,166 @@ type ProgressWriter struct { Total int64 Downloaded int64 mu sync.Mutex + TargetName string +} + +func NewProgressWriter(total int64, targetName string) *ProgressWriter { + return &ProgressWriter{Total: total, TargetName: targetName} } func (pw *ProgressWriter) Write(p []byte) (int, error) { n := len(p) pw.mu.Lock() pw.Downloaded += int64(n) - pw.PrintProgress() + pw.printProgress() pw.mu.Unlock() return n, nil } -func (pw *ProgressWriter) PrintProgress() { - fmt.Printf("\r > %s...: %d / %d bytes complete \t", defaultServerImageFilename, pw.Downloaded, pw.Total) +func (pw *ProgressWriter) printProgress() { + // Overwrite the same line with \r + fmt.Printf("\r > %s: %d / %d bytes complete", pw.TargetName, pw.Downloaded, pw.Total) } -func DownloadPart(url string, start, end int64, partFileName string, pw *ProgressWriter, wg *sync.WaitGroup, retryLimit int) error { - defer wg.Done() +// copyLocalFile copies from a local source path to destination, reporting progress. +func copyLocalFile(srcPath, dstPath string) error { + srcFile, err := os.Open(srcPath) + if err != nil { + return fmt.Errorf("failed to open local source file %q: %w", srcPath, err) + } + defer srcFile.Close() + + // Get total size + info, err := srcFile.Stat() + if err != nil { + return fmt.Errorf("failed to stat local source file %q: %w", srcPath, err) + } + totalSize := info.Size() + + outFile, err := os.Create(dstPath) + if err != nil { + return fmt.Errorf("failed to create destination file %q: %w", dstPath, err) + } + defer outFile.Close() + + pw := NewProgressWriter(totalSize, filepath.Base(dstPath)) + // Use TeeReader: read from srcFile, write to pw (for progress), and to outFile + _, err = io.Copy(outFile, io.TeeReader(srcFile, pw)) + if err != nil { + return fmt.Errorf("error copying local file to %q: %w", dstPath, err) + } + // Final newline after progress + fmt.Printf("\n") + log.Infof("\t> Local copy complete: %s\n", dstPath) + return nil +} + +// DownloadPart downloads a byte-range [start,end] of the URL into a temporary part file. +// On error, it returns the error; progress for the range is reported via pw. +// The caller goroutine must call wg.Done() exactly once. +func DownloadPart(urlStr string, start, end int64, partFileName string, pw *ProgressWriter, retryLimit int) error { var resp *http.Response + var err error // Retry loop for downloading a part for retries := 0; retries <= retryLimit; retries++ { - req, err := http.NewRequest("GET", url, nil) - if err != nil { - log.Errorf("Error creating request: %v", err) - return err + req, errReq := http.NewRequest("GET", urlStr, nil) + if errReq != nil { + log.Errorf("Error creating request: %v", errReq) + return errReq } req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end)) resp, err = http.DefaultClient.Do(req) if err != nil { - log.Errorf("Error downloading part: %v", err) - // Retry after waiting a bit - time.Sleep(time.Duration(2< retry log.Errorf("Error: expected status 206 Partial Content, got %d", resp.StatusCode) - // Retry after waiting - time.Sleep(time.Duration(2< 2 && localPath[1] == ':' { + localPath = localPath[1:] + } + return copyLocalFile(localPath, filePath) + } + } + // If no scheme or non-file scheme, check if it's a path existing on disk: + if fi, err := os.Stat(urlStr); err == nil && !fi.IsDir() { + // Treat as local file + return copyLocalFile(urlStr, filePath) + } + + // Otherwise assume HTTP(S) URL: + // First, HEAD to get total size + resp, err := http.Head(urlStr) if err != nil { - return fmt.Errorf("failed to get file info: %w", err) + return fmt.Errorf("failed to get file info from URL %q: %w", urlStr, err) } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return fmt.Errorf("bad status: %s", resp.Status) + return fmt.Errorf("bad status from HEAD %q: %s", urlStr, resp.Status) } - totalSize := resp.ContentLength - partSize := totalSize / int64(numParts) + if totalSize <= 0 { + return fmt.Errorf("unknown content length for URL %q", urlStr) + } - pw := &ProgressWriter{Total: totalSize} - var wg sync.WaitGroup + // For progress display: use base name of destination + pw := NewProgressWriter(totalSize, filepath.Base(filePath)) + // Partition download into numParts + partSize := totalSize / int64(numParts) + var wg sync.WaitGroup + var muErr sync.Mutex + downloadErrors := make([]error, 0, numParts) partFiles := make([]string, numParts) - var downloadErrors []error for i := 0; i < numParts; i++ { start := int64(i) * partSize @@ -106,48 +185,54 @@ func DownloadVHDX(url string, filePath string, numParts int, retryLimit int) err if i == numParts-1 { end = totalSize - 1 } - - partFileName := fmt.Sprintf("part-%d.tmp", i) + partFileName := fmt.Sprintf("%s.part-%d.tmp", filePath, i) partFiles[i] = partFileName wg.Add(1) - go func(i int) { - err := DownloadPart(url, start, end, partFileName, pw, &wg, retryLimit) - if err != nil { - downloadErrors = append(downloadErrors, fmt.Errorf("failed to download part %d: %w", i, err)) + go func(idx int, s, e int64, pfn string) { + defer wg.Done() + errPart := DownloadPart(urlStr, s, e, pfn, pw, retryLimit) + if errPart != nil { + muErr.Lock() + downloadErrors = append(downloadErrors, fmt.Errorf("part %d: %w", idx, errPart)) + muErr.Unlock() } - }(i) + }(i, start, end, partFileName) } + // Wait for all parts wg.Wait() - // If there are any errors during download, return them if len(downloadErrors) > 0 { - return fmt.Errorf("download failed for the following parts: %v", downloadErrors) + // Clean up partial files + for _, pfn := range partFiles { + os.Remove(pfn) + } + return fmt.Errorf("download failed for parts: %v", downloadErrors) } - // Proceed with merging the parts as before - out, err := os.Create(filePath) + // Merge parts + outFile, err := os.Create(filePath) if err != nil { - return fmt.Errorf("failed to create file: %w", err) + return fmt.Errorf("failed to create destination file %q: %w", filePath, err) } - defer out.Close() + defer outFile.Close() - for _, partFileName := range partFiles { - partFile, err := os.Open(partFileName) - if err != nil { - return fmt.Errorf("failed to open part file: %w", err) + for _, pfn := range partFiles { + f, errOpen := os.Open(pfn) + if errOpen != nil { + return fmt.Errorf("failed to open part file %q: %w", pfn, errOpen) } - - _, err = io.Copy(out, partFile) - partFile.Close() - if err != nil { - return fmt.Errorf("failed to merge part file: %w", err) + _, errCopy := io.Copy(outFile, f) + f.Close() + if errCopy != nil { + return fmt.Errorf("failed to merge part file %q: %w", pfn, errCopy) } - - os.Remove(partFileName) + os.Remove(pfn) } - log.Infof("\n\r\t> Download complete") + // Final newline after progress + fmt.Printf("\n") + log.Infof("\t> Download complete: %s\n", filePath) return nil } diff --git a/libmachine/mcnutils/utils.go b/libmachine/mcnutils/utils.go index 381e29a..da8f4f6 100644 --- a/libmachine/mcnutils/utils.go +++ b/libmachine/mcnutils/utils.go @@ -17,26 +17,58 @@ type MultiError struct { Errs []error } -var ConfigGuestOSUtil *GuestOSUtil - -type GuestOSUtil struct { - os string +// var ConfigGuestOSUtil *GuestOSUtil + +// type GuestOSUtil struct { +// os string +// } + +// func SetGuestOSUtil(guestOS string) { +// ConfigGuestOSUtil = &GuestOSUtil{ +// os: guestOS, +// } +// } + +// func (g *GuestOSUtil) GetGuestOS() string { +// if g == nil { +// log.Debugf("GuestOSUtil is not initialized") +// return "unknown" +// } +// return g.os +// } + +type GuestUtil struct { + os string + vhdUrl string } -func SetGuestOSUtil(guestOS string) { - ConfigGuestOSUtil = &GuestOSUtil{ - os: guestOS, +// ConfigGuest is the package-level singleton for GuestUtil +var ConfigGuest *GuestUtil + +func SetGuestUtil(guestOS, vhdUrl string) { + ConfigGuest = &GuestUtil{ + os: guestOS, + vhdUrl: vhdUrl, } + log.Debugf("SetGuestUtil: os=%s, vhdUrl=%s", guestOS, vhdUrl) } -func (g *GuestOSUtil) GetGuestOS() string { +func (g *GuestUtil) GetGuestOS() string { if g == nil { - log.Debugf("GuestOSUtil is not initialized") + log.Debugf("GuestUtil is not initialized") return "unknown" } return g.os } +func (g *GuestUtil) GetVHDUrl() string { + if g == nil { + log.Debugf("GuestUtil is not initialized") + return "" + } + return g.vhdUrl +} + func (e MultiError) Error() string { aggregate := "" for _, err := range e.Errs { @@ -111,7 +143,7 @@ func WaitForSpecificOrError(f func() (bool, error), maxAttempts int, waitInterva } time.Sleep(waitInterval) } - return fmt.Errorf("Maximum number of retries (%d) exceeded", maxAttempts) + return fmt.Errorf("maximum number of retries (%d) exceeded", maxAttempts) } func WaitForSpecific(f func() bool, maxAttempts int, waitInterval time.Duration) error { From 57dc1ce6e1cac016e68c3aee8e3ebd913230b224 Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Sat, 30 Aug 2025 18:54:25 +0100 Subject: [PATCH 14/16] remove the empty windows-specific branch --- drivers/hyperv/hyperv.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/drivers/hyperv/hyperv.go b/drivers/hyperv/hyperv.go index 70b3e88..8b94ecd 100644 --- a/drivers/hyperv/hyperv.go +++ b/drivers/hyperv/hyperv.go @@ -301,9 +301,8 @@ func (d *Driver) Create() error { } } - if mcnutils.ConfigGuest.GetGuestOS() == "windows" { - // === Windows === - } else { + 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 { From ef332414e5239c291dca5cbbc83c329e16b693a9 Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Sat, 30 Aug 2025 20:59:27 +0100 Subject: [PATCH 15/16] changed to writeSSHKeyToVHDX to ensure unmount failures are returned --- drivers/hyperv/hyperv.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/drivers/hyperv/hyperv.go b/drivers/hyperv/hyperv.go index 8b94ecd..37c4415 100644 --- a/drivers/hyperv/hyperv.go +++ b/drivers/hyperv/hyperv.go @@ -559,7 +559,7 @@ func (d *Driver) generateDiskImage() (string, error) { return diskImage, nil } -func writeSSHKeyToVHDX(vhdxPath, publicSSHKeyPath string) error { +func writeSSHKeyToVHDX(vhdxPath, publicSSHKeyPath string) (retErr error) { output, err := cmdOut( "powershell", "-Command", "(Get-DiskImage -ImagePath", quote(vhdxPath), "| Mount-DiskImage -PassThru) | Out-Null;", @@ -583,16 +583,16 @@ func writeSSHKeyToVHDX(vhdxPath, publicSSHKeyPath string) error { defer func() { if unmountErr := cmd("Dismount-DiskImage", "-ImagePath", quote(vhdxPath)); unmountErr != nil { - log.Warnf("Failed to unmount VHDX: %v", unmountErr) + 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 := ioutil.ReadFile(publicSSHKeyPath) + pubKey, err := os.ReadFile(publicSSHKeyPath) if err != nil { - return fmt.Errorf("failed to read public SSH key: %w", err) + return fmt.Errorf("failed to read public SSH key from %s: %w", publicSSHKeyPath, err) } if _, err := os.Stat(mountDir); os.IsNotExist(err) { @@ -607,9 +607,8 @@ func writeSSHKeyToVHDX(vhdxPath, publicSSHKeyPath string) error { return fmt.Errorf("failed to write public key: %w", err) } - err = cmd("icacls.exe", quote(adminAuthKeys), "/inheritance:r", "/grant", "Administrators:F", "/grant", "SYSTEM:F") - if err != nil { - return fmt.Errorf("failed to set permissions: %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 From 6e7b321d60363c345df7433ab4014a7681e0a84f Mon Sep 17 00:00:00 2001 From: Bob Sira Date: Tue, 2 Sep 2025 20:01:28 +0100 Subject: [PATCH 16/16] removed unwanted comments --- libmachine/mcnutils/utils.go | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/libmachine/mcnutils/utils.go b/libmachine/mcnutils/utils.go index da8f4f6..bd9e028 100644 --- a/libmachine/mcnutils/utils.go +++ b/libmachine/mcnutils/utils.go @@ -17,26 +17,6 @@ type MultiError struct { Errs []error } -// var ConfigGuestOSUtil *GuestOSUtil - -// type GuestOSUtil struct { -// os string -// } - -// func SetGuestOSUtil(guestOS string) { -// ConfigGuestOSUtil = &GuestOSUtil{ -// os: guestOS, -// } -// } - -// func (g *GuestOSUtil) GetGuestOS() string { -// if g == nil { -// log.Debugf("GuestOSUtil is not initialized") -// return "unknown" -// } -// return g.os -// } - type GuestUtil struct { os string vhdUrl string