// 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 ×ync, nil }