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 )