mirror of https://github.com/containers/podman.git
615 lines
16 KiB
Go
615 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) StopWithForce() error {
|
|
return vm.stop(true)
|
|
}
|
|
|
|
func (vm *VirtualMachine) Stop() error {
|
|
return vm.stop(false)
|
|
}
|
|
|
|
func (vm *VirtualMachine) stop(force bool) 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", force).
|
|
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 < 200; 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())
|
|
mem := MemorySettings{}
|
|
if err := vm.getMemorySettings(&mem); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
config := HyperVConfig{
|
|
Hardware: HardwareConfig{
|
|
// TODO we could implement a getProcessorSettings like we did for memory
|
|
CPUs: summary.NumberOfProcessors,
|
|
DiskPath: diskPath,
|
|
DiskSize: diskSize,
|
|
Memory: mem.Limit,
|
|
},
|
|
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 = config.Memory
|
|
ms.VirtualQuantity = 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)
|
|
}
|
|
|
|
func (vm *VirtualMachine) getMemorySettings(m *MemorySettings) error {
|
|
service, err := wmiext.NewLocalService(HyperVNamespace)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer service.Close()
|
|
return vm.fetchExistingResourceSettings(service, "Msvm_MemorySettingData", m)
|
|
}
|
|
|
|
// 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 {
|
|
if err := vm.getMemorySettings(mem); 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
|
|
}
|
|
|
|
vsms, err := srv.GetSingletonInstance("Msvm_VirtualSystemManagementService")
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
defer vsms.Close()
|
|
|
|
var (
|
|
job *wmiext.Instance
|
|
)
|
|
|
|
// https://learn.microsoft.com/en-us/windows/win32/hyperv_v2/cim-virtualsystemmanagementservice-destroysystem
|
|
if err := vsms.BeginInvoke("DestroySystem").
|
|
In("AffectedSystem", vm.Path()).
|
|
Execute().
|
|
Out("Job", &job).
|
|
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 {
|
|
if _, err := vm.remove(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Remove disk only if we were given one
|
|
if len(diskPath) > 0 {
|
|
if err := os.Remove(diskPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (vm *VirtualMachine) State() EnabledState {
|
|
return EnabledState(vm.EnabledState)
|
|
}
|
|
|
|
func (vm *VirtualMachine) IsStarting() bool {
|
|
return Starting.equal(vm.EnabledState)
|
|
}
|