Files
podman/vendor/github.com/containers/libhvee/pkg/hypervctl/vm.go
Brent Baude 4b8230119e HyperV: wait on stop
When using podman machine with hyperv, stop was releasing the terminal
back top the user prematurely.  This resulted in users being able to run
subsequent commands while the vm was still stopped.  Commands like
machine stop were prone to failing.

[NO NEW TESTS NEEDED]

Signed-off-by: Brent Baude <bbaude@redhat.com>
2023-04-18 08:02:48 -05:00

610 lines
16 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, errorMsg string, translate func(int) error) error {
var err error
switch res {
case 0:
return nil
case 4096:
err = wmiext.WaitJob(service, job)
defer job.Close()
default:
if translate != nil {
return translate(int(res))
}
return fmt.Errorf("%s (result code %d)", errorMsg, res)
}
if err != nil {
desc, _ := job.GetAsString("ErrorDescription")
desc = strings.Replace(desc, "\n", " ", -1)
return fmt.Errorf("%s: %w (%s)", errorMsg, 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))
}
// Wait for vm to actually *be* down
for i := 0; i < 25; i++ {
refreshVM, err := vm.vmm.GetMachine(vm.ElementName)
if err != nil {
return err
}
if refreshVM.State() == Disabled {
break
}
time.Sleep(50 * time.Millisecond)
}
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, "failed to start vm", nil)
}
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 := vm.fetchSystemSettingsInstance(service)
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 := 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 config.Network {
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) fetchSystemSettingsInstance(service *wmiext.Service) (*wmiext.Instance, error) {
// When a settings snapshot is taken there are multiple associations, use only the realized/active version
return service.FindFirstRelatedInstanceThrough(vm.Path(), "Msvm_VirtualSystemSettingData", "Msvm_SettingsDefineState")
}
func (vm *VirtualMachine) fetchExistingResourceSettings(service *wmiext.Service, resourceType string, resourceSettings interface{}) error {
const errFmt = "could not fetch resource settings (%s): %w"
// When a settings snapshot is taken there are multiple associations, use only the realized/active version
instance, err := vm.fetchSystemSettingsInstance(service)
if err != nil {
return fmt.Errorf(errFmt, resourceType, err)
}
defer instance.Close()
path, err := instance.Path()
if err != nil {
return fmt.Errorf(errFmt, resourceType, err)
}
return service.FindFirstRelatedObject(path, resourceType, resourceSettings)
}
// Update processor and/or mem
func (vm *VirtualMachine) UpdateProcessorMemSettings(updateProcessor func(*ProcessorSettings), updateMemory func(*MemorySettings)) error {
service, err := wmiext.NewLocalService(HyperVNamespace)
if err != nil {
return err
}
defer service.Close()
proc := &ProcessorSettings{}
mem := &MemorySettings{}
var settings []string
if updateProcessor != nil {
err = vm.fetchExistingResourceSettings(service, "Msvm_ProcessorSettingData", proc)
if err != nil {
return err
}
updateProcessor(proc)
processorStr, err := createProcessorSettings(proc)
if err != nil {
return err
}
settings = append(settings, processorStr)
}
if updateMemory != nil {
err = vm.fetchExistingResourceSettings(service, "Msvm_MemorySettingData", mem)
if err != nil {
return err
}
updateMemory(mem)
memStr, err := createMemorySettings(mem)
if err != nil {
return err
}
settings = append(settings, memStr)
}
if len(settings) < 1 {
return nil
}
vsms, err := service.GetSingletonInstance("Msvm_VirtualSystemManagementService")
if err != nil {
return err
}
defer vsms.Close()
var job *wmiext.Instance
var res int32
err = vsms.BeginInvoke("ModifyResourceSettings").
In("ResourceSettings", settings).
Execute().
Out("Job", &job).
Out("ReturnValue", &res).
End()
if err != nil {
return fmt.Errorf("failed to modify resource settings: %w", err)
}
return waitVMResult(res, service, job, "failed to modify resource settings", translateModifyError)
}
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 := vm.fetchSystemSettingsInstance(srv)
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 vsms.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, "failed to remove vm", nil); 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)
}