mirror of
https://github.com/containers/podman.git
synced 2025-12-09 15:19:35 +08:00
304 lines
8.1 KiB
Go
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 ×ync, nil
|
|
}
|