github.com/containers/podman/v4@v4.9.4/pkg/machine/qemu/config.go (about)

     1  package qemu
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/fs"
     7  	"os"
     8  	"path/filepath"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/containers/common/pkg/config"
    14  	"github.com/containers/podman/v4/pkg/machine"
    15  	"github.com/containers/podman/v4/pkg/machine/compression"
    16  	"github.com/containers/podman/v4/pkg/machine/define"
    17  	"github.com/containers/podman/v4/utils"
    18  	"github.com/docker/go-units"
    19  	"github.com/sirupsen/logrus"
    20  )
    21  
    22  type QEMUVirtualization struct {
    23  	machine.Virtualization
    24  }
    25  
    26  // setNewMachineCMDOpts are options needed to pass
    27  // into setting up the qemu command line.  long term, this need
    28  // should be eliminated
    29  // TODO Podman5
    30  type setNewMachineCMDOpts struct {
    31  	imageDir string
    32  }
    33  
    34  // findQEMUBinary locates and returns the QEMU binary
    35  func findQEMUBinary() (string, error) {
    36  	cfg, err := config.Default()
    37  	if err != nil {
    38  		return "", err
    39  	}
    40  	return cfg.FindHelperBinary(QemuCommand, true)
    41  }
    42  
    43  // setQMPMonitorSocket sets the virtual machine's QMP Monitor socket
    44  func (v *MachineVM) setQMPMonitorSocket() error {
    45  	monitor, err := NewQMPMonitor("unix", v.Name, defaultQMPTimeout)
    46  	if err != nil {
    47  		return err
    48  	}
    49  	v.QMPMonitor = monitor
    50  	return nil
    51  }
    52  
    53  // setNewMachineCMD configure the CLI command that will be run to create the new
    54  // machine
    55  func (v *MachineVM) setNewMachineCMD(qemuBinary string, cmdOpts *setNewMachineCMDOpts) {
    56  	v.CmdLine = NewQemuBuilder(qemuBinary, v.addArchOptions(cmdOpts))
    57  	v.CmdLine.SetMemory(v.Memory)
    58  	v.CmdLine.SetCPUs(v.CPUs)
    59  	v.CmdLine.SetIgnitionFile(v.IgnitionFile)
    60  	v.CmdLine.SetQmpMonitor(v.QMPMonitor)
    61  	v.CmdLine.SetNetwork()
    62  	v.CmdLine.SetSerialPort(v.ReadySocket, v.VMPidFilePath, v.Name)
    63  	v.CmdLine.SetUSBHostPassthrough(v.USBs)
    64  }
    65  
    66  func parseUSBs(usbs []string) ([]machine.USBConfig, error) {
    67  	configs := []machine.USBConfig{}
    68  	for _, str := range usbs {
    69  		if str == "" {
    70  			// Ignore --usb="" as it can be used to reset USBConfigs
    71  			continue
    72  		}
    73  
    74  		vals := strings.Split(str, ",")
    75  		if len(vals) != 2 {
    76  			return configs, fmt.Errorf("usb: fail to parse: missing ',': %s", str)
    77  		}
    78  
    79  		left := strings.Split(vals[0], "=")
    80  		if len(left) != 2 {
    81  			return configs, fmt.Errorf("usb: fail to parse: missing '=': %s", str)
    82  		}
    83  
    84  		right := strings.Split(vals[1], "=")
    85  		if len(right) != 2 {
    86  			return configs, fmt.Errorf("usb: fail to parse: missing '=': %s", str)
    87  		}
    88  
    89  		option := left[0] + "_" + right[0]
    90  
    91  		switch option {
    92  		case "bus_devnum", "devnum_bus":
    93  			bus, devnumber := left[1], right[1]
    94  			if right[0] == "bus" {
    95  				bus, devnumber = devnumber, bus
    96  			}
    97  
    98  			configs = append(configs, machine.USBConfig{
    99  				Bus:       bus,
   100  				DevNumber: devnumber,
   101  			})
   102  		case "vendor_product", "product_vendor":
   103  			vendorStr, productStr := left[1], right[1]
   104  			if right[0] == "vendor" {
   105  				vendorStr, productStr = productStr, vendorStr
   106  			}
   107  
   108  			vendor, err := strconv.ParseInt(vendorStr, 16, 0)
   109  			if err != nil {
   110  				return configs, fmt.Errorf("usb: fail to convert vendor of %s: %s", str, err)
   111  			}
   112  
   113  			product, err := strconv.ParseInt(productStr, 16, 0)
   114  			if err != nil {
   115  				return configs, fmt.Errorf("usb: fail to convert product of %s: %s", str, err)
   116  			}
   117  
   118  			configs = append(configs, machine.USBConfig{
   119  				Vendor:  int(vendor),
   120  				Product: int(product),
   121  			})
   122  		default:
   123  			return configs, fmt.Errorf("usb: fail to parse: %s", str)
   124  		}
   125  	}
   126  	return configs, nil
   127  }
   128  
   129  // NewMachine initializes an instance of a virtual machine based on the qemu
   130  // virtualization.
   131  func (p *QEMUVirtualization) NewMachine(opts machine.InitOptions) (machine.VM, error) {
   132  	vm := new(MachineVM)
   133  	if len(opts.Name) > 0 {
   134  		vm.Name = opts.Name
   135  	}
   136  
   137  	dataDir, err := machine.GetDataDir(p.VMType())
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	// set VM ignition file
   143  	if err := machine.SetIgnitionFile(&vm.IgnitionFile, vmtype, vm.Name); err != nil {
   144  		return nil, err
   145  	}
   146  
   147  	// set VM image file
   148  	imagePath, err := define.NewMachineFile(opts.ImagePath, nil)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  	vm.ImagePath = *imagePath
   153  
   154  	vm.RemoteUsername = opts.Username
   155  
   156  	// Add a random port for ssh
   157  	port, err := utils.GetRandomPort()
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  	vm.Port = port
   162  
   163  	vm.CPUs = opts.CPUS
   164  	vm.Memory = opts.Memory
   165  	vm.DiskSize = opts.DiskSize
   166  	if vm.USBs, err = parseUSBs(opts.USBs); err != nil {
   167  		return nil, err
   168  	}
   169  
   170  	vm.Created = time.Now()
   171  
   172  	// find QEMU binary
   173  	execPath, err := findQEMUBinary()
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	if err := vm.setPIDSocket(); err != nil {
   179  		return nil, err
   180  	}
   181  
   182  	// Add qmp socket
   183  	if err := vm.setQMPMonitorSocket(); err != nil {
   184  		return nil, err
   185  	}
   186  
   187  	runtimeDir, err := getRuntimeDir()
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  	symlink := vm.Name + "_ready.sock"
   192  	if err := machine.SetSocket(&vm.ReadySocket, machine.ReadySocketPath(runtimeDir+"/podman/", vm.Name), &symlink); err != nil {
   193  		return nil, err
   194  	}
   195  
   196  	// configure command to run
   197  	cmdOpts := setNewMachineCMDOpts{imageDir: dataDir}
   198  	vm.setNewMachineCMD(execPath, &cmdOpts)
   199  	return vm, nil
   200  }
   201  
   202  // LoadVMByName reads a json file that describes a known qemu vm
   203  // and returns a vm instance
   204  func (p *QEMUVirtualization) LoadVMByName(name string) (machine.VM, error) {
   205  	vm := &MachineVM{Name: name}
   206  	vm.HostUser = machine.HostUser{UID: -1} // posix reserves -1, so use it to signify undefined
   207  	if err := vm.update(); err != nil {
   208  		return nil, err
   209  	}
   210  
   211  	lock, err := machine.GetLock(vm.Name, vmtype)
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  	vm.lock = lock
   216  
   217  	return vm, nil
   218  }
   219  
   220  // List lists all vm's that use qemu virtualization
   221  func (p *QEMUVirtualization) List(_ machine.ListOptions) ([]*machine.ListResponse, error) {
   222  	return getVMInfos()
   223  }
   224  
   225  func getVMInfos() ([]*machine.ListResponse, error) {
   226  	vmConfigDir, err := machine.GetConfDir(vmtype)
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  
   231  	var listed []*machine.ListResponse
   232  
   233  	if err = filepath.WalkDir(vmConfigDir, func(path string, d fs.DirEntry, err error) error {
   234  		vm := new(MachineVM)
   235  		if strings.HasSuffix(d.Name(), ".json") {
   236  			fullPath := filepath.Join(vmConfigDir, d.Name())
   237  			b, err := os.ReadFile(fullPath)
   238  			if err != nil {
   239  				return err
   240  			}
   241  			err = json.Unmarshal(b, vm)
   242  			if err != nil {
   243  				// Checking if the file did not unmarshal because it is using
   244  				// the deprecated config file format.
   245  				migrateErr := migrateVM(fullPath, b, vm)
   246  				if migrateErr != nil {
   247  					return migrateErr
   248  				}
   249  			}
   250  			listEntry := new(machine.ListResponse)
   251  
   252  			listEntry.Name = vm.Name
   253  			listEntry.Stream = vm.ImageStream
   254  			listEntry.VMType = "qemu"
   255  			listEntry.CPUs = vm.CPUs
   256  			listEntry.Memory = vm.Memory * units.MiB
   257  			listEntry.DiskSize = vm.DiskSize * units.GiB
   258  			listEntry.Port = vm.Port
   259  			listEntry.RemoteUsername = vm.RemoteUsername
   260  			listEntry.IdentityPath = vm.IdentityPath
   261  			listEntry.CreatedAt = vm.Created
   262  			listEntry.Starting = vm.Starting
   263  			listEntry.UserModeNetworking = true // always true
   264  
   265  			if listEntry.CreatedAt.IsZero() {
   266  				listEntry.CreatedAt = time.Now()
   267  				vm.Created = time.Now()
   268  				if err := vm.writeConfig(); err != nil {
   269  					return err
   270  				}
   271  			}
   272  
   273  			state, err := vm.State(false)
   274  			if err != nil {
   275  				return err
   276  			}
   277  			listEntry.Running = state == machine.Running
   278  			listEntry.LastUp = vm.LastUp
   279  
   280  			listed = append(listed, listEntry)
   281  		}
   282  		return nil
   283  	}); err != nil {
   284  		return nil, err
   285  	}
   286  	return listed, err
   287  }
   288  
   289  func (p *QEMUVirtualization) IsValidVMName(name string) (bool, error) {
   290  	infos, err := getVMInfos()
   291  	if err != nil {
   292  		return false, err
   293  	}
   294  	for _, vm := range infos {
   295  		if vm.Name == name {
   296  			return true, nil
   297  		}
   298  	}
   299  	return false, nil
   300  }
   301  
   302  // CheckExclusiveActiveVM checks if there is a VM already running
   303  // that does not allow other VMs to be running
   304  func (p *QEMUVirtualization) CheckExclusiveActiveVM() (bool, string, error) {
   305  	vms, err := getVMInfos()
   306  	if err != nil {
   307  		return false, "", fmt.Errorf("checking VM active: %w", err)
   308  	}
   309  	// NOTE: Start() takes care of dealing with the "starting" state.
   310  	for _, vm := range vms {
   311  		if vm.Running {
   312  			return true, vm.Name, nil
   313  		}
   314  	}
   315  	return false, "", nil
   316  }
   317  
   318  // RemoveAndCleanMachines removes all machine and cleans up any other files associated with podman machine
   319  func (p *QEMUVirtualization) RemoveAndCleanMachines() error {
   320  	var (
   321  		vm             machine.VM
   322  		listResponse   []*machine.ListResponse
   323  		opts           machine.ListOptions
   324  		destroyOptions machine.RemoveOptions
   325  	)
   326  	destroyOptions.Force = true
   327  	var prevErr error
   328  
   329  	listResponse, err := p.List(opts)
   330  	if err != nil {
   331  		return err
   332  	}
   333  
   334  	for _, mach := range listResponse {
   335  		vm, err = p.LoadVMByName(mach.Name)
   336  		if err != nil {
   337  			if prevErr != nil {
   338  				logrus.Error(prevErr)
   339  			}
   340  			prevErr = err
   341  		}
   342  		_, remove, err := vm.Remove(mach.Name, destroyOptions)
   343  		if err != nil {
   344  			if prevErr != nil {
   345  				logrus.Error(prevErr)
   346  			}
   347  			prevErr = err
   348  		} else {
   349  			if err := remove(); err != nil {
   350  				if prevErr != nil {
   351  					logrus.Error(prevErr)
   352  				}
   353  				prevErr = err
   354  			}
   355  		}
   356  	}
   357  
   358  	// Clean leftover files in data dir
   359  	dataDir, err := machine.DataDirPrefix()
   360  	if err != nil {
   361  		if prevErr != nil {
   362  			logrus.Error(prevErr)
   363  		}
   364  		prevErr = err
   365  	} else {
   366  		err := utils.GuardedRemoveAll(dataDir)
   367  		if err != nil {
   368  			if prevErr != nil {
   369  				logrus.Error(prevErr)
   370  			}
   371  			prevErr = err
   372  		}
   373  	}
   374  
   375  	// Clean leftover files in conf dir
   376  	confDir, err := machine.ConfDirPrefix()
   377  	if err != nil {
   378  		if prevErr != nil {
   379  			logrus.Error(prevErr)
   380  		}
   381  		prevErr = err
   382  	} else {
   383  		err := utils.GuardedRemoveAll(confDir)
   384  		if err != nil {
   385  			if prevErr != nil {
   386  				logrus.Error(prevErr)
   387  			}
   388  			prevErr = err
   389  		}
   390  	}
   391  	return prevErr
   392  }
   393  
   394  func (p *QEMUVirtualization) VMType() machine.VMType {
   395  	return vmtype
   396  }
   397  
   398  func VirtualizationProvider() machine.VirtProvider {
   399  	return &QEMUVirtualization{
   400  		machine.NewVirtualization(define.Qemu, compression.Xz, define.Qcow, vmtype),
   401  	}
   402  }
   403  
   404  // Deprecated: MachineVMV1 is being deprecated in favor a more flexible and informative
   405  // structure
   406  type MachineVMV1 struct {
   407  	// CPUs to be assigned to the VM
   408  	CPUs uint64
   409  	// The command line representation of the qemu command
   410  	CmdLine []string
   411  	// Mounts is the list of remote filesystems to mount
   412  	Mounts []machine.Mount
   413  	// IdentityPath is the fq path to the ssh priv key
   414  	IdentityPath string
   415  	// IgnitionFilePath is the fq path to the .ign file
   416  	IgnitionFilePath string
   417  	// ImageStream is the update stream for the image
   418  	ImageStream string
   419  	// ImagePath is the fq path to
   420  	ImagePath string
   421  	// Memory in megabytes assigned to the vm
   422  	Memory uint64
   423  	// Disk size in gigabytes assigned to the vm
   424  	DiskSize uint64
   425  	// Name of the vm
   426  	Name string
   427  	// SSH port for user networking
   428  	Port int
   429  	// QMPMonitor is the qemu monitor object for sending commands
   430  	QMPMonitor Monitorv1
   431  	// RemoteUsername of the vm user
   432  	RemoteUsername string
   433  	// Whether this machine should run in a rootful or rootless manner
   434  	Rootful bool
   435  	// UID is the numerical id of the user that called machine
   436  	UID int
   437  }
   438  
   439  type Monitorv1 struct {
   440  	//	Address portion of the qmp monitor (/tmp/tmp.sock)
   441  	Address string
   442  	// Network portion of the qmp monitor (unix)
   443  	Network string
   444  	// Timeout in seconds for qmp monitor transactions
   445  	Timeout time.Duration
   446  }
   447  
   448  var (
   449  	// defaultQMPTimeout is the timeout duration for the
   450  	// qmp monitor interactions.
   451  	defaultQMPTimeout = 2 * time.Second
   452  )