Files
podman/vendor/github.com/crc-org/vfkit/pkg/config/config.go
renovate[bot] 9de7e07e56 fix(deps): update module github.com/crc-org/vfkit to v0.6.0
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-29 11:55:43 +00:00

304 lines
8.1 KiB
Go

// Package config provides native go data types to describe a VM configuration
// (memory, CPUs, bootloader, devices, ...).
// It's used by vfkit which generates a VirtualMachine instance after parsing
// its command line using FromOptions().
// It can also be used by application writers who want to start a VM with
// vfkit. After creating a VirtualMachine instance with the needed devices,
// calling VirtualMachine.Cmd() will return an exec.Cmd which can be used
// to start the virtual machine.
//
// This package does not use Code-Hex/vz directly as it must possible to
// cross-compile code using it.
package config
import (
"fmt"
"math"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"github.com/containers/common/pkg/strongunits"
)
// VirtualMachine is the top-level type. It describes the virtual machine
// configuration (bootloader, devices, ...).
type VirtualMachine struct {
Vcpus uint `json:"vcpus"`
Memory strongunits.B `json:"memoryBytes"`
Bootloader Bootloader `json:"bootloader"`
Devices []VirtioDevice `json:"devices,omitempty"`
Timesync *TimeSync `json:"timesync,omitempty"`
Ignition *Ignition `json:"ignition,omitempty"`
}
// TimeSync enables synchronization of the host time to the linux guest after the host was suspended.
// This requires qemu-guest-agent to be running in the guest, and to be listening on a vsock socket
type TimeSync struct {
VsockPort uint32 `json:"vsockPort"`
}
type Ignition struct {
ConfigPath string `json:"configPath"`
SocketPath string `json:"socketPath"`
}
// The VMComponent interface represents a VM element (device, bootloader, ...)
// which can be converted from/to commandline parameters
type VMComponent interface {
FromOptions([]option) error
ToCmdLine() ([]string, error)
}
const (
ignitionVsockPort uint = 1024
ignitionSocketName string = "ignition.sock"
)
// NewVirtualMachine creates a new VirtualMachine instance. The virtual machine
// will use vcpus virtual CPUs and it will be allocated memoryMiB mibibytes
// (1024*1024 bytes) of RAM. bootloader specifies how the virtual machine will
// be booted (UEFI or with the specified kernel/initrd/commandline)
func NewVirtualMachine(vcpus uint, memoryMiB uint64, bootloader Bootloader) *VirtualMachine {
return &VirtualMachine{
Vcpus: vcpus,
Memory: strongunits.MiB(memoryMiB).ToBytes(),
Bootloader: bootloader,
}
}
// round value up to the nearest mibibyte multiple
func roundToMiB(value strongunits.StorageUnits) strongunits.MiB {
mib := uint64(strongunits.MiB(1).ToBytes())
valueB := strongunits.B(uint64(value.ToBytes()) + mib - 1)
return strongunits.ToMib(valueB)
}
// ToCmdLine generates a list of arguments for use with the [os/exec] package.
// These arguments will start a virtual machine with the devices/bootloader/...
// described by vm If the virtual machine configuration described by vm is
// invalid, an error will be returned.
func (vm *VirtualMachine) ToCmdLine() ([]string, error) {
// TODO: missing binary name/path
args := []string{}
if vm.Vcpus != 0 {
args = append(args, "--cpus", strconv.FormatUint(uint64(vm.Vcpus), 10))
}
if uint64(vm.Memory.ToBytes()) != 0 {
args = append(args, "--memory", strconv.FormatUint(uint64(roundToMiB(vm.Memory)), 10))
}
if vm.Bootloader == nil {
return nil, fmt.Errorf("missing bootloader configuration")
}
bootloaderArgs, err := vm.Bootloader.ToCmdLine()
if err != nil {
return nil, err
}
args = append(args, bootloaderArgs...)
for _, dev := range vm.Devices {
devArgs, err := dev.ToCmdLine()
if err != nil {
return nil, err
}
args = append(args, devArgs...)
}
if vm.Ignition != nil {
args = append(args, "--ignition", vm.Ignition.ConfigPath)
}
return args, nil
}
func (vm *VirtualMachine) extraFiles() []*os.File {
extraFiles := []*os.File{}
for _, dev := range vm.Devices {
virtioNet, ok := dev.(*VirtioNet)
if !ok {
continue
}
if virtioNet.Socket != nil {
extraFiles = append(extraFiles, virtioNet.Socket)
}
}
return extraFiles
}
// Cmd creates an exec.Cmd to start vfkit with the configured devices.
// In particular it will set ExtraFiles appropriately when mapping
// a file with a network interface.
func (vm *VirtualMachine) Cmd(vfkitPath string) (*exec.Cmd, error) {
args, err := vm.ToCmdLine()
if err != nil {
return nil, err
}
cmd := exec.Command(vfkitPath, args...)
cmd.ExtraFiles = vm.extraFiles()
return cmd, nil
}
func (vm *VirtualMachine) AddDevicesFromCmdLine(cmdlineOpts []string) error {
for _, deviceOpts := range cmdlineOpts {
dev, err := deviceFromCmdLine(deviceOpts)
if err != nil {
return err
}
vm.Devices = append(vm.Devices, dev)
}
return nil
}
func (vm *VirtualMachine) VirtioGPUDevices() []*VirtioGPU {
gpuDevs := []*VirtioGPU{}
for _, dev := range vm.Devices {
if gpuDev, isVirtioGPU := dev.(*VirtioGPU); isVirtioGPU {
gpuDevs = append(gpuDevs, gpuDev)
}
}
return gpuDevs
}
func (vm *VirtualMachine) VirtioVsockDevices() []*VirtioVsock {
vsockDevs := []*VirtioVsock{}
for _, dev := range vm.Devices {
if vsockDev, isVirtioVsock := dev.(*VirtioVsock); isVirtioVsock {
vsockDevs = append(vsockDevs, vsockDev)
}
}
return vsockDevs
}
func (vm *VirtualMachine) NetworkBlockDevice(deviceID string) *NetworkBlockDevice {
for _, dev := range vm.Devices {
if nbdDev, isNbdDev := dev.(*NetworkBlockDevice); isNbdDev && nbdDev.DeviceIdentifier == deviceID {
return nbdDev
}
}
return nil
}
// AddDevice adds a dev to vm. This device can be created with one of the
// VirtioXXXNew methods.
func (vm *VirtualMachine) AddDevice(dev VirtioDevice) error {
return vm.AddDevices(dev)
}
// AddDevices adds a list of devices to vm.
func (vm *VirtualMachine) AddDevices(dev ...VirtioDevice) error {
vm.Devices = append(vm.Devices, dev...)
return nil
}
func (vm *VirtualMachine) AddTimeSyncFromCmdLine(cmdlineOpts string) error {
if cmdlineOpts == "" {
return nil
}
timesync, err := timesyncFromCmdLine(cmdlineOpts)
if err != nil {
return err
}
vm.Timesync = timesync
return nil
}
func (vm *VirtualMachine) TimeSync() *TimeSync {
return vm.Timesync
}
func IgnitionNew(configPath string, socketPath string) (*Ignition, error) {
if configPath == "" || socketPath == "" {
return nil, fmt.Errorf("config path and socket path cannot be empty")
}
return &Ignition{
ConfigPath: configPath,
SocketPath: socketPath,
}, nil
}
func (vm *VirtualMachine) AddIgnitionFileFromCmdLine(cmdlineOpts string) error {
if cmdlineOpts == "" {
return nil
}
opts := strings.Split(cmdlineOpts, ",")
if len(opts) != 1 {
return fmt.Errorf("ignition only accept one option in command line argument")
}
socketPath := filepath.Join(os.TempDir(), ignitionSocketName)
dev, err := VirtioVsockNew(ignitionVsockPort, socketPath, true)
if err != nil {
return err
}
vm.Devices = append(vm.Devices, dev)
ignition, err := IgnitionNew(opts[0], socketPath)
if err != nil {
return err
}
vm.Ignition = ignition
return nil
}
func TimeSyncNew(vsockPort uint) (VMComponent, error) {
if vsockPort > math.MaxUint32 {
return nil, fmt.Errorf("invalid vsock port: %d", vsockPort)
}
return &TimeSync{
VsockPort: uint32(vsockPort), //#nosec G115 -- was compared to math.MaxUint32
}, nil
}
func (ts *TimeSync) ToCmdLine() ([]string, error) {
args := []string{}
if ts.VsockPort != 0 {
args = append(args, fmt.Sprintf("vsockPort=%d", ts.VsockPort))
}
return []string{"--timesync", strings.Join(args, ",")}, nil
}
func (ts *TimeSync) FromOptions(options []option) error {
for _, option := range options {
switch option.key {
case "vsockPort":
vsockPort, err := strconv.ParseUint(option.value, 10, 32)
if err != nil {
return err
}
ts.VsockPort = uint32(vsockPort) //#nosec G115 -- ParseUint(_, _, 32) guarantees no overflow
default:
return fmt.Errorf("unknown option for timesync parameter: %s", option.key)
}
}
if ts.VsockPort == 0 {
return fmt.Errorf("missing 'vsockPort' option for timesync parameter")
}
return nil
}
func timesyncFromCmdLine(optsStr string) (*TimeSync, error) {
var timesync TimeSync
optsStrv := strings.Split(optsStr, ",")
options := strvToOptions(optsStrv)
if err := timesync.FromOptions(options); err != nil {
return nil, err
}
return &timesync, nil
}