spiegel_podman/vendor/github.com/containers/libhvee/pkg/hypervctl/vm.go
Brent Baude 0dac214f56 basic hypverv machine implementation
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>
2023-03-17 16:02:28 -05:00

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)
}