mirror of https://github.com/containers/podman.git
459 lines
12 KiB
Go
459 lines
12 KiB
Go
//go:build windows
|
|
// +build windows
|
|
|
|
package hyperv
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/containers/libhvee/pkg/hypervctl"
|
|
"github.com/containers/podman/v4/pkg/machine"
|
|
"github.com/containers/storage/pkg/homedir"
|
|
"github.com/docker/go-units"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var (
|
|
// vmtype refers to qemu (vs libvirt, krun, etc).
|
|
vmtype = machine.HyperVVirt
|
|
)
|
|
|
|
func GetVirtualizationProvider() machine.VirtProvider {
|
|
return &Virtualization{
|
|
artifact: machine.HyperV,
|
|
compression: machine.Zip,
|
|
format: machine.Vhdx,
|
|
}
|
|
}
|
|
|
|
const (
|
|
// Some of this will need to change when we are closer to having
|
|
// working code.
|
|
VolumeTypeVirtfs = "virtfs"
|
|
MountType9p = "9p"
|
|
dockerSock = "/var/run/docker.sock"
|
|
dockerConnectTimeout = 5 * time.Second
|
|
apiUpTimeout = 20 * time.Second
|
|
)
|
|
|
|
type apiForwardingState int
|
|
|
|
const (
|
|
noForwarding apiForwardingState = iota
|
|
claimUnsupported
|
|
notInstalled
|
|
machineLocal
|
|
dockerGlobal
|
|
)
|
|
|
|
type HyperVMachine struct {
|
|
// copied from qemu, cull and add as needed
|
|
|
|
// ConfigPath is the fully qualified path to the configuration file
|
|
ConfigPath machine.VMFile
|
|
// The command line representation of the qemu command
|
|
//CmdLine []string
|
|
// HostUser contains info about host user
|
|
machine.HostUser
|
|
// ImageConfig describes the bootable image
|
|
machine.ImageConfig
|
|
// Mounts is the list of remote filesystems to mount
|
|
Mounts []machine.Mount
|
|
// Name of VM
|
|
Name string
|
|
// PidFilePath is the where the Proxy PID file lives
|
|
//PidFilePath machine.VMFile
|
|
// VMPidFilePath is the where the VM PID file lives
|
|
//VMPidFilePath machine.VMFile
|
|
// QMPMonitor is the qemu monitor object for sending commands
|
|
//QMPMonitor Monitor
|
|
// ReadySocket tells host when vm is booted
|
|
ReadySocket machine.VMFile
|
|
// ResourceConfig is physical attrs of the VM
|
|
machine.ResourceConfig
|
|
// SSHConfig for accessing the remote vm
|
|
machine.SSHConfig
|
|
// Starting tells us whether the machine is running or if we have just dialed it to start it
|
|
Starting bool
|
|
// Created contains the original created time instead of querying the file mod time
|
|
Created time.Time
|
|
// LastUp contains the last recorded uptime
|
|
LastUp time.Time
|
|
}
|
|
|
|
func (m *HyperVMachine) Init(opts machine.InitOptions) (bool, error) {
|
|
var (
|
|
key string
|
|
)
|
|
|
|
sshDir := filepath.Join(homedir.Get(), ".ssh")
|
|
m.IdentityPath = filepath.Join(sshDir, m.Name)
|
|
|
|
if len(opts.IgnitionPath) < 1 {
|
|
uri := machine.SSHRemoteConnection.MakeSSHURL("localhost", fmt.Sprintf("/run/user/%d/podman/podman.sock", m.UID), strconv.Itoa(m.Port), m.RemoteUsername)
|
|
uriRoot := machine.SSHRemoteConnection.MakeSSHURL("localhost", "/run/podman/podman.sock", strconv.Itoa(m.Port), "root")
|
|
identity := filepath.Join(sshDir, m.Name)
|
|
|
|
uris := []url.URL{uri, uriRoot}
|
|
names := []string{m.Name, m.Name + "-root"}
|
|
|
|
// The first connection defined when connections is empty will become the default
|
|
// regardless of IsDefault, so order according to rootful
|
|
if opts.Rootful {
|
|
uris[0], names[0], uris[1], names[1] = uris[1], names[1], uris[0], names[0]
|
|
}
|
|
|
|
for i := 0; i < 2; i++ {
|
|
if err := machine.AddConnection(&uris[i], names[i], identity, opts.IsDefault && i == 0); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
} else {
|
|
fmt.Println("An ignition path was provided. No SSH connection was added to Podman")
|
|
}
|
|
if len(opts.IgnitionPath) < 1 {
|
|
var err error
|
|
key, err = machine.CreateSSHKeys(m.IdentityPath)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
m.ResourceConfig = machine.ResourceConfig{
|
|
CPUs: opts.CPUS,
|
|
DiskSize: opts.DiskSize,
|
|
Memory: opts.Memory,
|
|
}
|
|
|
|
// If the user provides an ignition file, we need to
|
|
// copy it into the conf dir
|
|
if len(opts.IgnitionPath) > 0 {
|
|
inputIgnition, err := os.ReadFile(opts.IgnitionPath)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return false, os.WriteFile(m.IgnitionFile.GetPath(), inputIgnition, 0644)
|
|
}
|
|
|
|
// Write the JSON file for the second time. First time was in NewMachine
|
|
b, err := json.MarshalIndent(m, "", " ")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if err := os.WriteFile(m.ConfigPath.GetPath(), b, 0644); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if m.UID == 0 {
|
|
m.UID = 1000
|
|
}
|
|
|
|
// c/common sets the default machine user for "windows" to be "user"; this
|
|
// is meant for the WSL implementation that does not use FCOS. For FCOS,
|
|
// however, we want to use the DefaultIgnitionUserName which is currently
|
|
// "core"
|
|
user := opts.Username
|
|
if user == "user" {
|
|
user = machine.DefaultIgnitionUserName
|
|
}
|
|
// Write the ignition file
|
|
ign := machine.DynamicIgnition{
|
|
Name: user,
|
|
Key: key,
|
|
VMName: m.Name,
|
|
TimeZone: opts.TimeZone,
|
|
WritePath: m.IgnitionFile.GetPath(),
|
|
UID: m.UID,
|
|
}
|
|
|
|
if err := machine.NewIgnitionFile(ign, machine.HyperVVirt); err != nil {
|
|
return false, err
|
|
}
|
|
// The ignition file has been written. We now need to
|
|
// read it so that we can put it into key-value pairs
|
|
ignFile, err := m.IgnitionFile.Read()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
reader := bytes.NewReader(ignFile)
|
|
|
|
vm, err := hypervctl.NewVirtualMachineManager().GetMachine(m.Name)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
err = vm.SplitAndAddIgnition("ignition.config.", reader)
|
|
return err == nil, err
|
|
}
|
|
|
|
func (m *HyperVMachine) Inspect() (*machine.InspectInfo, error) {
|
|
vm, err := hypervctl.NewVirtualMachineManager().GetMachine(m.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg, err := vm.GetConfig(m.ImagePath.GetPath())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &machine.InspectInfo{
|
|
ConfigPath: m.ConfigPath,
|
|
ConnectionInfo: machine.ConnectionConfig{},
|
|
Created: m.Created,
|
|
Image: machine.ImageConfig{
|
|
IgnitionFile: machine.VMFile{},
|
|
ImageStream: "",
|
|
ImagePath: machine.VMFile{},
|
|
},
|
|
LastUp: m.LastUp,
|
|
Name: m.Name,
|
|
Resources: machine.ResourceConfig{
|
|
CPUs: uint64(cfg.Hardware.CPUs),
|
|
DiskSize: 0,
|
|
Memory: uint64(cfg.Hardware.Memory),
|
|
},
|
|
SSHConfig: m.SSHConfig,
|
|
State: vm.State().String(),
|
|
}, nil
|
|
}
|
|
|
|
func (m *HyperVMachine) Remove(_ string, opts machine.RemoveOptions) (string, func() error, error) {
|
|
var (
|
|
files []string
|
|
diskPath string
|
|
)
|
|
vmm := hypervctl.NewVirtualMachineManager()
|
|
vm, err := vmm.GetMachine(m.Name)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
// In hyperv, they call running 'enabled'
|
|
if vm.State() == hypervctl.Enabled {
|
|
if !opts.Force {
|
|
return "", nil, hypervctl.ErrMachineStateInvalid
|
|
}
|
|
if err := vm.Stop(); err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
|
|
// Collect all the files that need to be destroyed
|
|
if !opts.SaveKeys {
|
|
files = append(files, m.IdentityPath, m.IdentityPath+".pub")
|
|
}
|
|
if !opts.SaveIgnition {
|
|
files = append(files, m.IgnitionFile.GetPath())
|
|
}
|
|
|
|
if !opts.SaveImage {
|
|
diskPath := m.ImagePath.GetPath()
|
|
files = append(files, diskPath)
|
|
}
|
|
|
|
if err := machine.RemoveConnection(m.Name); err != nil {
|
|
logrus.Error(err)
|
|
}
|
|
if err := machine.RemoveConnection(m.Name + "-root"); err != nil {
|
|
logrus.Error(err)
|
|
}
|
|
files = append(files, getVMConfigPath(m.ConfigPath.GetPath(), m.Name))
|
|
confirmationMessage := "\nThe following files will be deleted:\n\n"
|
|
for _, msg := range files {
|
|
confirmationMessage += msg + "\n"
|
|
}
|
|
|
|
confirmationMessage += "\n"
|
|
return confirmationMessage, func() error {
|
|
for _, f := range files {
|
|
if err := os.Remove(f); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
logrus.Error(err)
|
|
}
|
|
}
|
|
return vm.Remove(diskPath)
|
|
}, nil
|
|
}
|
|
|
|
func (m *HyperVMachine) Set(name string, opts machine.SetOptions) ([]error, error) {
|
|
var (
|
|
cpuChanged, memoryChanged bool
|
|
setErrors []error
|
|
)
|
|
vmm := hypervctl.NewVirtualMachineManager()
|
|
// Considering this a hard return if we cannot lookup the machine
|
|
vm, err := vmm.GetMachine(m.Name)
|
|
if err != nil {
|
|
return setErrors, err
|
|
}
|
|
if vm.State() != hypervctl.Disabled {
|
|
return nil, errors.New("unable to change settings unless vm is stopped")
|
|
}
|
|
|
|
if opts.Rootful != nil && m.Rootful != *opts.Rootful {
|
|
setErrors = append(setErrors, hypervctl.ErrNotImplemented)
|
|
}
|
|
if opts.DiskSize != nil && m.DiskSize != *opts.DiskSize {
|
|
setErrors = append(setErrors, hypervctl.ErrNotImplemented)
|
|
}
|
|
if opts.CPUs != nil && m.CPUs != *opts.CPUs {
|
|
m.CPUs = *opts.CPUs
|
|
cpuChanged = true
|
|
}
|
|
if opts.Memory != nil && m.Memory != *opts.Memory {
|
|
m.Memory = *opts.Memory
|
|
memoryChanged = true
|
|
}
|
|
|
|
if !cpuChanged && !memoryChanged {
|
|
switch len(setErrors) {
|
|
case 0:
|
|
return nil, nil
|
|
case 1:
|
|
return nil, setErrors[0]
|
|
default:
|
|
return setErrors[1:], setErrors[0]
|
|
}
|
|
}
|
|
// Write the new JSON out
|
|
// considering this a hard return if we cannot write the JSON file.
|
|
b, err := json.MarshalIndent(m, "", " ")
|
|
if err != nil {
|
|
return setErrors, err
|
|
}
|
|
if err := os.WriteFile(m.ConfigPath.GetPath(), b, 0644); err != nil {
|
|
return setErrors, err
|
|
}
|
|
|
|
return setErrors, vm.UpdateProcessorMemSettings(func(ps *hypervctl.ProcessorSettings) {
|
|
if cpuChanged {
|
|
ps.VirtualQuantity = m.CPUs
|
|
}
|
|
}, func(ms *hypervctl.MemorySettings) {
|
|
if memoryChanged {
|
|
ms.DynamicMemoryEnabled = false
|
|
ms.VirtualQuantity = m.Memory
|
|
ms.Limit = m.Memory
|
|
ms.Reservation = m.Memory
|
|
}
|
|
})
|
|
}
|
|
|
|
func (m *HyperVMachine) SSH(name string, opts machine.SSHOptions) error {
|
|
return machine.ErrNotImplemented
|
|
}
|
|
|
|
func (m *HyperVMachine) Start(name string, opts machine.StartOptions) error {
|
|
// TODO We need to hold Start until it actually finishes booting and ignition stuff
|
|
vmm := hypervctl.NewVirtualMachineManager()
|
|
vm, err := vmm.GetMachine(m.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if vm.State() != hypervctl.Disabled {
|
|
return hypervctl.ErrMachineStateInvalid
|
|
}
|
|
return vm.Start()
|
|
}
|
|
|
|
func (m *HyperVMachine) State(_ bool) (machine.Status, error) {
|
|
vmm := hypervctl.NewVirtualMachineManager()
|
|
vm, err := vmm.GetMachine(m.Name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if vm.IsStarting() {
|
|
return machine.Starting, nil
|
|
}
|
|
if vm.State() == hypervctl.Enabled {
|
|
return machine.Running, nil
|
|
}
|
|
// Following QEMU pattern here where only three
|
|
// states seem valid
|
|
return machine.Stopped, nil
|
|
}
|
|
|
|
func (m *HyperVMachine) Stop(name string, opts machine.StopOptions) error {
|
|
vmm := hypervctl.NewVirtualMachineManager()
|
|
vm, err := vmm.GetMachine(m.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if vm.State() != hypervctl.Enabled {
|
|
return hypervctl.ErrMachineStateInvalid
|
|
}
|
|
return vm.Stop()
|
|
}
|
|
|
|
func (m *HyperVMachine) jsonConfigPath() (string, error) {
|
|
configDir, err := machine.GetConfDir(machine.HyperVVirt)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return getVMConfigPath(configDir, m.Name), nil
|
|
}
|
|
|
|
func (m *HyperVMachine) loadFromFile() (*HyperVMachine, error) {
|
|
if len(m.Name) < 1 {
|
|
return nil, errors.New("encountered machine with no name")
|
|
}
|
|
|
|
jsonPath, err := m.jsonConfigPath()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mm := HyperVMachine{}
|
|
|
|
if err := loadMacMachineFromJSON(jsonPath, &mm); err != nil {
|
|
return nil, err
|
|
}
|
|
vmm := hypervctl.NewVirtualMachineManager()
|
|
vm, err := vmm.GetMachine(m.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg, err := vm.GetConfig(mm.ImagePath.GetPath())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If the machine is on, we can get what it is actually using
|
|
if cfg.Hardware.CPUs > 0 {
|
|
mm.CPUs = uint64(cfg.Hardware.CPUs)
|
|
}
|
|
// Same for memory
|
|
if cfg.Hardware.Memory > 0 {
|
|
mm.Memory = uint64(cfg.Hardware.Memory)
|
|
}
|
|
|
|
mm.DiskSize = cfg.Hardware.DiskSize * units.MiB
|
|
mm.LastUp = cfg.Status.LastUp
|
|
|
|
return &mm, nil
|
|
}
|
|
|
|
// getVMConfigPath is a simple wrapper for getting the fully-qualified
|
|
// path of the vm json config file. It should be used to get conformity
|
|
func getVMConfigPath(configDir, vmName string) string {
|
|
return filepath.Join(configDir, fmt.Sprintf("%s.json", vmName))
|
|
}
|
|
|
|
func loadMacMachineFromJSON(fqConfigPath string, macMachine *HyperVMachine) error {
|
|
b, err := os.ReadFile(fqConfigPath)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("%q: %w", fqConfigPath, machine.ErrNoSuchVM)
|
|
}
|
|
return err
|
|
}
|
|
return json.Unmarshal(b, macMachine)
|
|
}
|