mirror of
https://github.com/podman-container-tools/podman.git
synced 2026-09-25 00:57:58 +00:00
with libhvee, we are able to do the basics of podman machine management on hyperv. The basic functions like init, rm, stop, and start are all functional. Start and stop will periodically throw a benign error processing the hyperv message being returned from the action. The error is described in the todo's below. notable items: * no podman commands will work (like ps, images, etc) * the machine must be initialized with --image-path and fed a custom image. * disk size is set to 100GB statically. * the vm joins the default hyperv network which is TCP/IP network based. * podman machine ssh does not work * podman machine set does not work * you can grab the ip address from hyperv and fake a machine connection with `podman system connection`. * when booting, use the hyperv console to know the boot is complete. TODOs: * podman machine ssh * podman machine set * podman machine rm needs force bool * disk size in NewMachine is set to 100GB * podman start needs to wait until fully booted * establish a boot complete signal from guest * implement gvproxy like user networking * fix benign failures in stop/start -> Error: error 2147749890 (FormatMessage failed with: The system cannot find message text for message number 0x%1 in the message file for %2.) [NO NEW TESTS NEEDED] Signed-off-by: Brent Baude <bbaude@redhat.com>
498 lines
13 KiB
Go
498 lines
13 KiB
Go
//go:build windows
|
|
// +build windows
|
|
|
|
package hypervctl
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/containers/libhvee/pkg/kvp/ginsu"
|
|
"github.com/containers/libhvee/pkg/wmiext"
|
|
)
|
|
|
|
// delete this when close to being done
|
|
var (
|
|
ErrNotImplemented = errors.New("function not implemented")
|
|
)
|
|
|
|
type VirtualMachine struct {
|
|
S__PATH string `json:"-"`
|
|
S__CLASS string `json:"-"`
|
|
InstanceID string
|
|
Caption string
|
|
Description string
|
|
ElementName string
|
|
InstallDate time.Time
|
|
OperationalStatus []uint16
|
|
StatusDescriptions []string
|
|
Status string
|
|
HealthState uint16
|
|
CommunicationStatus uint16
|
|
DetailedStatus uint16
|
|
OperatingStatus uint16
|
|
PrimaryStatus uint16
|
|
EnabledState uint16
|
|
OtherEnabledState string
|
|
RequestedState uint16
|
|
EnabledDefault uint16
|
|
TimeOfLastStateChange string
|
|
AvailableRequestedStates []uint16
|
|
TransitioningToState uint16
|
|
CreationClassName string
|
|
Name string
|
|
PrimaryOwnerName string
|
|
PrimaryOwnerContact string
|
|
Roles []string
|
|
NameFormat string
|
|
OtherIdentifyingInfo []string
|
|
IdentifyingDescriptions []string
|
|
Dedicated []uint16
|
|
OtherDedicatedDescriptions []string
|
|
ResetCapability uint16
|
|
PowerManagementCapabilities []uint16
|
|
OnTimeInMilliseconds uint64
|
|
ProcessID uint32
|
|
TimeOfLastConfigurationChange string
|
|
NumberOfNumaNodes uint16
|
|
ReplicationState uint16
|
|
ReplicationHealth uint16
|
|
ReplicationMode uint16
|
|
FailedOverReplicationType uint16
|
|
LastReplicationType uint16
|
|
LastApplicationConsistentReplicationTime string
|
|
LastReplicationTime time.Time
|
|
LastSuccessfulBackupTime string
|
|
EnhancedSessionModeState uint16
|
|
vmm *VirtualMachineManager
|
|
}
|
|
|
|
func (vm *VirtualMachine) Path() string {
|
|
return vm.S__PATH
|
|
}
|
|
func (vm *VirtualMachine) SplitAndAddIgnition(keyPrefix string, ignRdr *bytes.Reader) error {
|
|
parts, err := ginsu.Dice(ignRdr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for idx, val := range parts {
|
|
key := fmt.Sprintf("%s%d", keyPrefix, idx)
|
|
if err := vm.AddKeyValuePair(key, val); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (vm *VirtualMachine) AddKeyValuePair(key string, value string) error {
|
|
return vm.kvpOperation("AddKvpItems", key, value, "key already exists?")
|
|
}
|
|
|
|
func (vm *VirtualMachine) ModifyKeyValuePair(key string, value string) error {
|
|
return vm.kvpOperation("ModifyKvpItems", key, value, "key invalid?")
|
|
}
|
|
|
|
func (vm *VirtualMachine) PutKeyValuePair(key string, value string) error {
|
|
err := vm.AddKeyValuePair(key, value)
|
|
kvpError, ok := err.(*KvpError)
|
|
if !ok || kvpError.ErrorCode != KvpIllegalArgument {
|
|
return err
|
|
}
|
|
|
|
return vm.ModifyKeyValuePair(key, value)
|
|
}
|
|
|
|
func (vm *VirtualMachine) RemoveKeyValuePair(key string) error {
|
|
return vm.kvpOperation("RemoveKvpItems", key, "", "key invalid?")
|
|
}
|
|
|
|
func (vm *VirtualMachine) GetKeyValuePairs() (map[string]string, error) {
|
|
var service *wmiext.Service
|
|
var err error
|
|
|
|
if service, err = wmiext.NewLocalService(HyperVNamespace); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer service.Close()
|
|
|
|
i, err := service.FindFirstRelatedInstance(vm.Path(), "Msvm_KvpExchangeComponent")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer i.Close()
|
|
|
|
var path string
|
|
path, err = i.GetAsString("__PATH")
|
|
if err != nil {
|
|
return nil, err
|
|
|
|
}
|
|
|
|
i, err = service.FindFirstRelatedInstance(path, "Msvm_KvpExchangeComponentSettingData")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer i.Close()
|
|
|
|
s, err := i.GetAsString("HostExchangeItems")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return parseKvpMapXml(s)
|
|
}
|
|
|
|
func (vm *VirtualMachine) kvpOperation(op string, key string, value string, illegalSuggestion string) error {
|
|
var service *wmiext.Service
|
|
var vsms, job *wmiext.Instance
|
|
var err error
|
|
|
|
if service, err = wmiext.NewLocalService(HyperVNamespace); err != nil {
|
|
return err
|
|
}
|
|
defer service.Close()
|
|
|
|
vsms, err = service.GetSingletonInstance(VirtualSystemManagementService)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer vsms.Close()
|
|
|
|
itemStr, err := createKvpItem(service, key, value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
execution := vsms.BeginInvoke(op).
|
|
In("TargetSystem", vm.Path()).
|
|
In("DataItems", []string{itemStr}).
|
|
Execute()
|
|
|
|
if err := execution.Out("Job", &job).End(); err != nil {
|
|
return fmt.Errorf("%s execution failed: %w", op, err)
|
|
}
|
|
|
|
err = translateKvpError(wmiext.WaitJob(service, job), illegalSuggestion)
|
|
defer job.Close()
|
|
return err
|
|
}
|
|
|
|
func waitVMResult(res int32, service *wmiext.Service, job *wmiext.Instance) error {
|
|
var err error
|
|
|
|
if res == 4096 {
|
|
err = wmiext.WaitJob(service, job)
|
|
defer job.Close()
|
|
}
|
|
|
|
if err != nil {
|
|
desc, _ := job.GetAsString("ErrorDescription")
|
|
desc = strings.Replace(desc, "\n", " ", -1)
|
|
return fmt.Errorf("failed to define system: %w (%s)", err, desc)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (vm *VirtualMachine) Stop() error {
|
|
if !Enabled.equal(vm.EnabledState) {
|
|
return ErrMachineNotRunning
|
|
}
|
|
var (
|
|
err error
|
|
res int32
|
|
srv *wmiext.Service
|
|
)
|
|
if srv, err = wmiext.NewLocalService(HyperVNamespace); err != nil {
|
|
return err
|
|
}
|
|
wmiInst, err := srv.FindFirstRelatedInstance(vm.Path(), "Msvm_ShutdownComponent")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// https://learn.microsoft.com/en-us/windows/win32/hyperv_v2/msvm-shutdowncomponent-initiateshutdown
|
|
err = wmiInst.BeginInvoke("InitiateShutdown").
|
|
In("Reason", "User requested").
|
|
In("Force", false).
|
|
Execute().
|
|
Out("ReturnValue", &res).End()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if res != 0 {
|
|
return translateShutdownError(int(res))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (vm *VirtualMachine) Start() error {
|
|
var (
|
|
srv *wmiext.Service
|
|
err error
|
|
job *wmiext.Instance
|
|
res int32
|
|
)
|
|
|
|
if s := vm.EnabledState; !Disabled.equal(s) {
|
|
if Enabled.equal(s) {
|
|
return ErrMachineAlreadyRunning
|
|
} else if Starting.equal(s) {
|
|
return ErrMachineAlreadyRunning
|
|
}
|
|
return errors.New("machine not in a state to start")
|
|
}
|
|
|
|
if srv, err = getService(srv); err != nil {
|
|
return err
|
|
}
|
|
defer srv.Close()
|
|
|
|
instance, err := srv.GetObject(vm.Path())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer instance.Close()
|
|
|
|
// https://learn.microsoft.com/en-us/windows/win32/hyperv_v2/cim-concretejob-requeststatechange
|
|
if err := instance.BeginInvoke("RequestStateChange").
|
|
In("RequestedState", uint16(start)).
|
|
In("TimeoutPeriod", &time.Time{}).
|
|
Execute().
|
|
Out("Job", &job).
|
|
Out("ReturnValue", &res).End(); err != nil {
|
|
return err
|
|
}
|
|
return waitVMResult(res, srv, job)
|
|
}
|
|
|
|
func getService(_ *wmiext.Service) (*wmiext.Service, error) {
|
|
// any reason why when we instantiate a vm, we should NOT just embed a service?
|
|
return wmiext.NewLocalService(HyperVNamespace)
|
|
}
|
|
|
|
func (vm *VirtualMachine) list() ([]*HyperVConfig, error) {
|
|
|
|
return nil, ErrNotImplemented
|
|
}
|
|
|
|
func (vm *VirtualMachine) GetConfig(diskPath string) (*HyperVConfig, error) {
|
|
var (
|
|
diskSize uint64
|
|
)
|
|
summary, err := vm.GetSummaryInformation(SummaryRequestCommon)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Grabbing actual disk size
|
|
diskPathInfo, err := os.Stat(diskPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
diskSize = uint64(diskPathInfo.Size())
|
|
|
|
config := HyperVConfig{
|
|
Hardware: HardwareConfig{
|
|
CPUs: summary.NumberOfProcessors,
|
|
DiskPath: diskPath,
|
|
DiskSize: diskSize,
|
|
Memory: summary.MemoryAvailable,
|
|
},
|
|
Status: Statuses{
|
|
Created: vm.InstallDate,
|
|
LastUp: time.Time{},
|
|
Running: Enabled.equal(vm.EnabledState),
|
|
Starting: vm.IsStarting(),
|
|
State: EnabledState(vm.EnabledState),
|
|
},
|
|
}
|
|
return &config, nil
|
|
}
|
|
|
|
// GetSummaryInformation returns the live VM summary information for this virtual machine.
|
|
// The requestedFields parameter controls which fields of summary information are populated.
|
|
// SummaryRequestCommon and SummaryRequestNearAll provide predefined combinations for this
|
|
// parameter
|
|
func (vm *VirtualMachine) GetSummaryInformation(requestedFields SummaryRequestSet) (*SummaryInformation, error) {
|
|
service, err := wmiext.NewLocalService(HyperVNamespace)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer service.Close()
|
|
|
|
instance, err := service.FindFirstRelatedInstance(vm.Path(), "Msvm_VirtualSystemSettingData")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer instance.Close()
|
|
|
|
path, err := instance.Path()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := vm.vmm.getSummaryInformation(path, requestedFields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(result) < 1 {
|
|
return nil, errors.New("summary information search returned an empty result set")
|
|
}
|
|
|
|
return &result[0], nil
|
|
}
|
|
|
|
// NewVirtualMachine creates a new vm in hyperv
|
|
// decided to not return a *VirtualMachine here because of how Podman is
|
|
// likely to use this. this could be easily added if desirable
|
|
func (vmm *VirtualMachineManager) NewVirtualMachine(name string, config *HardwareConfig) error {
|
|
exists, err := vmm.Exists(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if exists {
|
|
return ErrMachineAlreadyExists
|
|
}
|
|
|
|
// TODO I gotta believe there are naming restrictions for vms in hyperv?
|
|
// TODO If something fails during creation, do we rip things down or follow precedent from other machines? user deletes things
|
|
|
|
systemSettings, err := NewSystemSettingsBuilder().
|
|
PrepareSystemSettings(name, nil).
|
|
PrepareMemorySettings(func(ms *MemorySettings) {
|
|
//ms.DynamicMemoryEnabled = false
|
|
//ms.VirtualQuantity = 8192 // Startup memory
|
|
//ms.Reservation = config.Memory // min
|
|
|
|
// The API seems to require both of these even
|
|
// when not using dynamic memory
|
|
ms.Limit = uint64(config.Memory)
|
|
ms.VirtualQuantity = uint64(config.Memory)
|
|
}).
|
|
PrepareProcessorSettings(func(ps *ProcessorSettings) {
|
|
ps.VirtualQuantity = uint64(config.CPUs) // 4 cores
|
|
}).
|
|
Build()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
//if err := vmm.CreateVhdxFile(config.DiskPath, config.DiskSize*1024*1024*1024); err != nil {
|
|
// return err
|
|
//}
|
|
if err := NewDriveSettingsBuilder(systemSettings).
|
|
AddScsiController().
|
|
AddSyntheticDiskDrive(0).
|
|
DefineVirtualHardDisk(config.DiskPath, func(vhdss *VirtualHardDiskStorageSettings) {
|
|
// set extra params like
|
|
// vhdss.IOPSLimit = 5000
|
|
}).
|
|
Finish(). // disk
|
|
Finish(). // drive
|
|
//AddSyntheticDvdDrive(1).
|
|
//DefineVirtualDvdDisk(isoFile).
|
|
//Finish(). // disk
|
|
//Finish(). // drive
|
|
Finish(). // controller
|
|
Complete(); err != nil {
|
|
return err
|
|
}
|
|
// Add default network connection
|
|
if err := NewNetworkSettingsBuilder(systemSettings).
|
|
AddSyntheticEthernetPort(nil).
|
|
AddEthernetPortAllocation(""). // "" = connect to default switch
|
|
Finish(). // allocation
|
|
Finish(). // port
|
|
Complete(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (vm *VirtualMachine) remove() (int32, error) {
|
|
var (
|
|
err error
|
|
res int32
|
|
srv *wmiext.Service
|
|
)
|
|
|
|
// Check for disabled/stopped state
|
|
if !Disabled.equal(vm.EnabledState) {
|
|
return -1, ErrMachineStateInvalid
|
|
}
|
|
if srv, err = wmiext.NewLocalService(HyperVNamespace); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
wmiInst, err := srv.FindFirstRelatedInstance(vm.Path(), "Msvm_VirtualSystemSettingData")
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
defer wmiInst.Close()
|
|
|
|
path, err := wmiInst.Path()
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
vsms, err := srv.GetSingletonInstance("Msvm_VirtualSystemManagementService")
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
defer wmiInst.Close()
|
|
|
|
var (
|
|
job *wmiext.Instance
|
|
resultingSystem string
|
|
)
|
|
// https://learn.microsoft.com/en-us/windows/win32/hyperv_v2/cim-virtualsystemmanagementservice-destroysystem
|
|
if err := vsms.BeginInvoke("DestroySystem").
|
|
In("AffectedSystem", path).
|
|
Execute().
|
|
Out("Job", &job).
|
|
Out("ResultingSystem", &resultingSystem).
|
|
Out("ReturnValue", &res).End(); err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
// do i have this correct? you can get an error without a result?
|
|
if err := waitVMResult(res, srv, job); err != nil {
|
|
return -1, err
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (vm *VirtualMachine) Remove(diskPath string) error {
|
|
res, err := vm.remove()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if DestroySystemResult(res) == VMDestroyCompletedwithNoError {
|
|
// Remove disk only if we were given one
|
|
if len(diskPath) > 0 {
|
|
if err := os.Remove(diskPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("failed to destroy system %s: %s", vm.Name, DestroySystemResult(res).Reason())
|
|
|
|
}
|
|
|
|
func (vm *VirtualMachine) State() EnabledState {
|
|
return EnabledState(vm.EnabledState)
|
|
}
|
|
|
|
func (vm *VirtualMachine) IsStarting() bool {
|
|
return Starting.equal(vm.EnabledState)
|
|
}
|