This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / spindle / engines / microvm / qemu.go
18 kB 753 lines
1//go:build linux 2 3package microvm 4 5import ( 6 "context" 7 "crypto/sha256" 8 _ "embed" 9 "encoding/hex" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "log/slog" 14 "os" 15 "os/exec" 16 "path/filepath" 17 "strconv" 18 "strings" 19 "sync" 20 "time" 21 22 "github.com/digitalocean/go-qemu/qmp" 23 "github.com/google/uuid" 24) 25 26const ( 27 defaultQMPTimeout = 10 * time.Second 28 outerSlirpCIDR = "10.0.2.0/24" 29 innerSlirpNet = "10.0.3.0/24" 30 innerSlirpHost = "10.0.3.2" 31 innerSlirpDNS = "10.0.3.3" 32 innerSlirpDHCP = "10.0.3.15" 33 netnsTapName = "tap0" 34 netnsMTU = "65520" 35) 36 37type QEMUConfig struct { 38 Image ImageSpec 39 BootTimeout time.Duration 40 CID uint32 41 EnableKVM bool 42 QEMULogPath string 43 QMPPath string 44 SerialLogPath string 45 WorkDir string 46 VolumePaths map[string]string 47 VolumeBaseName string 48 Cgroup CgroupLimits 49 Dev bool 50} 51 52type QEMUVMHandle struct { 53 cid uint32 54 Process *os.Process 55 qemuLogPath string 56 QMPMon *qmp.SocketMonitor 57 QMPPath string 58 serialLogPath string 59 workDir string 60 61 qmpSocketPath string 62 63 cmd *exec.Cmd 64 done chan struct{} 65 qemuLogFile *os.File 66 cgroup *CgroupHandle 67 slirpCmd *exec.Cmd 68 slirpExit *os.File 69 waitErr error 70 waitErrMu sync.Mutex 71} 72 73type qemuRunner struct{} 74 75func (qemuRunner) Validate(spec ImageSpec, enableKVM bool) error { 76 if _, err := exec.LookPath(spec.RunnerCmd()); err != nil { 77 return fmt.Errorf("required host command %q not found in PATH: %w", spec.RunnerCmd(), err) 78 } 79 if _, err := os.Stat("/dev/vhost-vsock"); err != nil { 80 return fmt.Errorf("microvm requires /dev/vhost-vsock for vhost-vsock-device: %w", err) 81 } 82 if enableKVM { 83 if _, err := os.Stat("/dev/kvm"); err != nil { 84 return fmt.Errorf("microvm KVM was requested but /dev/kvm is not accessible: %w", err) 85 } 86 } 87 if len(spec.NetworkInterfaces) > 0 { 88 if _, err := os.Stat("/dev/net/tun"); err != nil { 89 return fmt.Errorf("microvm slirp4netns networking requires /dev/net/tun: %w", err) 90 } 91 for _, cmd := range []string{"ip", "mount", "slirp4netns", "unshare"} { 92 if _, err := exec.LookPath(cmd); err != nil { 93 return fmt.Errorf("required host command %q not found in PATH: %w", cmd, err) 94 } 95 } 96 } 97 return nil 98} 99 100func (qemuRunner) Start(ctx context.Context, cfg VMConfig, volumePaths map[string]string, logger *slog.Logger) (VMHandle, error) { 101 bootTimeout := cfg.BootTimeout 102 if bootTimeout == 0 { 103 bootTimeout = 10 * time.Second 104 } 105 return StartQEMU(ctx, QEMUConfig{ 106 Image: cfg.Image, 107 BootTimeout: bootTimeout, 108 CID: cfg.CID, 109 EnableKVM: cfg.EnableKVM, 110 WorkDir: cfg.WorkDir, 111 VolumePaths: volumePaths, 112 Cgroup: cfg.Cgroup, 113 Dev: cfg.Dev, 114 }, logger) 115} 116 117// we hash the workDir to get a deterministic qmpSock path 118// since they are AF_UNIX sockets, they are bound by a 108 char long path limit... 119func qmpSocketPath(workDir string) string { 120 base := filepath.Dir(workDir) 121 if workDir == "" { 122 base = os.TempDir() 123 } 124 sum := sha256.Sum256([]byte(workDir)) 125 return filepath.Join(base, hex.EncodeToString(sum[:8])+".qmp.sock") 126} 127 128func StartQEMU(ctx context.Context, cfg QEMUConfig, logger *slog.Logger) (VMHandle, error) { 129 if logger == nil { 130 logger = slog.Default() 131 } 132 133 workDir := cfg.WorkDir 134 135 handle := &QEMUVMHandle{ 136 workDir: workDir, 137 } 138 139 var ok bool 140 defer func() { 141 if !ok { 142 if detail := VMCrashLog(handle); detail != "" { 143 logger.Error("microVM failed to start", "cid", handle.cid, "detail", detail) 144 } 145 _ = handle.Close() 146 } 147 }() 148 149 cid := cfg.CID 150 if cid == 0 { 151 var err error 152 cid, err = AllocateCID() 153 if err != nil { 154 return nil, err 155 } 156 } 157 if cid < minGuestCID { 158 return nil, fmt.Errorf("guest CID must be >= %d", minGuestCID) 159 } 160 handle.cid = cid 161 162 volumePaths := cfg.VolumePaths 163 164 qemuLogPath := cfg.QEMULogPath 165 if qemuLogPath == "" { 166 qemuLogPath = filepath.Join(workDir, "qemu.log") 167 } 168 qemuLogFile, err := createParentedFile(qemuLogPath) 169 if err != nil { 170 return nil, err 171 } 172 handle.qemuLogPath = qemuLogPath 173 handle.qemuLogFile = qemuLogFile 174 175 serialLogPath := cfg.SerialLogPath 176 if serialLogPath == "" { 177 serialLogPath = filepath.Join(workDir, "serial.log") 178 } 179 if err := os.MkdirAll(filepath.Dir(serialLogPath), 0o755); err != nil { 180 return nil, fmt.Errorf("create serial log directory: %w", err) 181 } 182 handle.serialLogPath = serialLogPath 183 184 qmpPath := cfg.QMPPath 185 if qmpPath == "" { 186 qmpPath = qmpSocketPath(workDir) 187 handle.qmpSocketPath = qmpPath 188 } 189 handle.QMPPath = qmpPath 190 191 qemuCmd := cfg.Image.RunnerCmd() 192 qemuBinary, err := exec.LookPath(qemuCmd) 193 if err != nil { 194 return nil, fmt.Errorf("%s command not found in PATH: %w", qemuCmd, err) 195 } 196 197 args, err := qemuArgs(qemuArgsConfig{ 198 Image: cfg.Image, 199 CID: cid, 200 EnableKVM: cfg.EnableKVM, 201 QMPPath: qmpPath, 202 SerialLogPath: serialLogPath, 203 VolumePaths: volumePaths, 204 }) 205 if err != nil { 206 return nil, err 207 } 208 209 cmd, slirpNet, err := qemuCommand(ctx, qemuBinary, args, cfg.Image, workDir, cfg.Dev) 210 if err != nil { 211 return nil, err 212 } 213 cmd.Env = append(os.Environ(), "TMPDIR="+workDir) 214 cmd.Stdout = qemuLogFile 215 cmd.Stderr = qemuLogFile 216 217 cgroup, err := prepareCgroup(cfg.Cgroup, logger) 218 if err != nil { 219 return nil, err 220 } 221 handle.cgroup = cgroup 222 223 logger.Info("starting qemu microvm", "cid", cid, "workDir", workDir, "serialLog", serialLogPath, "qmp", qmpPath) 224 if err := cmd.Start(); err != nil { 225 return nil, fmt.Errorf("starting qemu: %w", err) 226 } 227 handle.cmd = cmd 228 handle.Process = cmd.Process 229 handle.done = make(chan struct{}) 230 go func() { 231 err := cmd.Wait() 232 handle.waitErrMu.Lock() 233 handle.waitErr = err 234 handle.waitErrMu.Unlock() 235 close(handle.done) 236 }() 237 238 if err := cgroup.AddProcess(cmd.Process.Pid, logger); err != nil { 239 return nil, err 240 } 241 242 if slirpNet != nil { 243 handle.slirpCmd, handle.slirpExit, err = slirpNet.Start(ctx, qemuLogFile, logger) 244 if err != nil { 245 return nil, err 246 } 247 if handle.slirpCmd != nil && handle.slirpCmd.Process != nil { 248 if err := cgroup.AddProcess(handle.slirpCmd.Process.Pid, logger); err != nil { 249 return nil, err 250 } 251 } 252 } 253 254 qmpTimeout := cfg.BootTimeout 255 if qmpTimeout == 0 { 256 qmpTimeout = defaultQMPTimeout 257 } 258 qmpCtx, cancelQMP := context.WithTimeout(ctx, qmpTimeout) 259 defer cancelQMP() 260 if err := handle.waitForQMP(qmpCtx, qmpTimeout); err != nil { 261 return nil, err 262 } 263 264 status, err := handle.waitForQMPRunning(qmpCtx, qmpTimeout) 265 if err != nil { 266 return nil, err 267 } 268 logger.Info("qemu microvm running", "cid", cid, "status", status) 269 270 ok = true 271 return handle, nil 272} 273 274func (h *QEMUVMHandle) Wait() error { 275 if h == nil || h.done == nil { 276 return nil 277 } 278 <-h.done 279 h.waitErrMu.Lock() 280 defer h.waitErrMu.Unlock() 281 return h.waitErr 282} 283 284func (h *QEMUVMHandle) WaitContext(ctx context.Context) error { 285 if h == nil || h.done == nil { 286 return nil 287 } 288 select { 289 case <-h.done: 290 h.waitErrMu.Lock() 291 defer h.waitErrMu.Unlock() 292 return h.waitErr 293 case <-ctx.Done(): 294 return ctx.Err() 295 } 296} 297 298func (h *QEMUVMHandle) Kill() error { 299 if h == nil || h.Process == nil { 300 return nil 301 } 302 return h.Process.Kill() 303} 304 305func (h *QEMUVMHandle) Shutdown(ctx context.Context) error { 306 if h == nil { 307 return nil 308 } 309 if h.QMPMon != nil { 310 if err := h.QMPSystemPowerdown(); err != nil { 311 // dead qmp socket means qemu exited concurrently so we wait 312 select { 313 case <-h.done: 314 return nil 315 case <-ctx.Done(): 316 return err 317 } 318 } 319 } 320 if h.done == nil { 321 return nil 322 } 323 select { 324 case <-h.done: 325 return h.Wait() 326 case <-ctx.Done(): 327 _ = h.Kill() 328 _ = h.Wait() 329 return ctx.Err() 330 } 331} 332 333func (h *QEMUVMHandle) Close() error { 334 if h == nil { 335 return nil 336 } 337 338 var closeErr error 339 if h.QMPMon != nil { 340 closeErr = errors.Join(closeErr, h.QMPMon.Disconnect()) 341 h.QMPMon = nil 342 } 343 if h.Process != nil { 344 _ = h.Process.Kill() 345 _ = h.Wait() 346 } 347 if h.slirpExit != nil { 348 _ = h.slirpExit.Close() 349 h.slirpExit = nil 350 } 351 if h.slirpCmd != nil && h.slirpCmd.Process != nil { 352 _ = h.slirpCmd.Process.Kill() 353 _ = h.slirpCmd.Wait() 354 h.slirpCmd = nil 355 } 356 if h.qemuLogFile != nil { 357 closeErr = errors.Join(closeErr, h.qemuLogFile.Close()) 358 h.qemuLogFile = nil 359 } 360 if h.cgroup != nil { 361 closeErr = errors.Join(closeErr, h.cgroup.Close()) 362 h.cgroup = nil 363 } 364 if h.qmpSocketPath != "" { 365 if err := os.Remove(h.qmpSocketPath); err != nil && !os.IsNotExist(err) { 366 closeErr = errors.Join(closeErr, err) 367 } 368 h.qmpSocketPath = "" 369 } 370 return closeErr 371} 372 373func (h *QEMUVMHandle) QMPRun(command qmp.Command) ([]byte, error) { 374 if h == nil || h.QMPMon == nil { 375 return nil, fmt.Errorf("qmp monitor is not connected") 376 } 377 data, err := json.Marshal(command) 378 if err != nil { 379 return nil, err 380 } 381 return h.QMPMon.Run(data) 382} 383 384func (h *QEMUVMHandle) QMPQueryStatus() (string, error) { 385 raw, err := h.QMPRun(qmp.Command{Execute: "query-status"}) 386 if err != nil { 387 return "", fmt.Errorf("qmp query-status failed: %w", err) 388 } 389 390 var resp struct { 391 Return *struct { 392 Status string `json:"status"` 393 } `json:"return"` 394 } 395 if err := json.Unmarshal(raw, &resp); err != nil { 396 return "", fmt.Errorf("qmp query-status parse %q: %w", raw, err) 397 } 398 if resp.Return == nil { 399 return "", fmt.Errorf("qmp query-status missing return: %s", raw) 400 } 401 if resp.Return.Status == "" { 402 return "", fmt.Errorf("qmp query-status missing return.status: %s", raw) 403 } 404 return resp.Return.Status, nil 405} 406 407func (h *QEMUVMHandle) waitForQMPRunning(ctx context.Context, timeout time.Duration) (string, error) { 408 statusCtx, cancel := context.WithTimeout(ctx, timeout) 409 defer cancel() 410 411 ticker := time.NewTicker(25 * time.Millisecond) 412 defer ticker.Stop() 413 414 var lastStatus string 415 var lastErr error 416 417 for { 418 status, err := h.QMPQueryStatus() 419 if err != nil { 420 lastErr = err 421 } else { 422 lastErr = nil 423 lastStatus = status 424 425 switch status { 426 case "running": 427 return status, nil 428 case "shutdown", "internal-error", "io-error", "guest-panicked": 429 return "", fmt.Errorf("qemu guest entered unhealthy state before running (status: %s)", status) 430 } 431 } 432 433 select { 434 case <-statusCtx.Done(): 435 if lastErr != nil { 436 return "", fmt.Errorf("qemu guest did not reach running state (last status: %s): %w", lastStatus, errors.Join(statusCtx.Err(), lastErr)) 437 } 438 return "", fmt.Errorf("qemu guest did not reach running state (last status: %s): %w", lastStatus, statusCtx.Err()) 439 case <-h.done: 440 return "", fmt.Errorf("qemu exited before reaching running state: %w", h.Wait()) 441 case <-ticker.C: 442 } 443 } 444} 445 446func (h *QEMUVMHandle) QMPSystemPowerdown() error { 447 _, err := h.QMPRun(qmp.Command{Execute: "system_powerdown"}) 448 return err 449} 450 451func (h *QEMUVMHandle) Logs() VMLogs { 452 if h == nil { 453 return VMLogs{} 454 } 455 return VMLogs{ 456 Serial: h.serialLogPath, 457 Extra: map[string]string{ 458 "qemu": h.qemuLogPath, 459 }, 460 } 461} 462 463func (h *QEMUVMHandle) CID() uint32 { 464 if h == nil { 465 return 0 466 } 467 return h.cid 468} 469 470func (h *QEMUVMHandle) WorkDir() string { 471 if h == nil { 472 return "" 473 } 474 return h.workDir 475} 476 477func (h *QEMUVMHandle) OOMKilled() bool { 478 if h == nil { 479 return false 480 } 481 return h.cgroup.OOMKilled() 482} 483 484func (h *QEMUVMHandle) waitForQMP(ctx context.Context, timeout time.Duration) error { 485 qmpCtx, cancel := context.WithTimeout(ctx, timeout) 486 defer cancel() 487 488 var lastErr error 489 for { 490 mon, err := qmp.NewSocketMonitor("unix", h.QMPPath, 2*time.Second) 491 if err == nil { 492 if err = mon.Connect(); err == nil { 493 h.QMPMon = mon 494 return nil 495 } 496 _ = mon.Disconnect() 497 } 498 lastErr = err 499 500 select { 501 case <-qmpCtx.Done(): 502 return fmt.Errorf("qmp connect timeout: %w", lastErr) 503 case <-h.done: 504 return fmt.Errorf("qemu exited before qmp was ready: %w", h.Wait()) 505 case <-time.After(25 * time.Millisecond): 506 } 507 } 508} 509 510func qemuCommand( 511 ctx context.Context, 512 qemuBinary string, 513 args []string, 514 spec ImageSpec, 515 workDir string, 516 dev bool, 517) (*exec.Cmd, *slirpNamespace, error) { 518 if len(spec.NetworkInterfaces) == 0 { 519 return exec.CommandContext(ctx, qemuBinary, args...), nil, nil 520 } 521 522 ipPath, err := exec.LookPath("ip") 523 if err != nil { 524 return nil, nil, fmt.Errorf("ip command not found in PATH: %w", err) 525 } 526 mountPath, err := exec.LookPath("mount") 527 if err != nil { 528 return nil, nil, fmt.Errorf("mount command not found in PATH: %w", err) 529 } 530 unsharePath, err := exec.LookPath("unshare") 531 if err != nil { 532 return nil, nil, fmt.Errorf("unshare command not found in PATH: %w", err) 533 } 534 535 pidFile, resolvPath, wrapperPath, err := prepareQEMUNetnsFiles(workDir, dev) 536 if err != nil { 537 return nil, nil, err 538 } 539 540 cmdArgs := append([]string{ 541 "--user", 542 "--map-root-user", 543 "--net", 544 "--mount", 545 "--propagation", "private", 546 "--", 547 wrapperPath, 548 pidFile, 549 ipPath, 550 mountPath, 551 resolvPath, 552 qemuBinary, 553 }, args...) 554 555 cmd := exec.CommandContext(ctx, unsharePath, cmdArgs...) 556 557 return cmd, &slirpNamespace{ 558 spec: spec, 559 pidFile: pidFile, 560 dev: dev, 561 }, nil 562} 563 564func prepareQEMUNetnsFiles(workDir string, dev bool) (pidFile, resolvPath, wrapperPath string, err error) { 565 pidFile = filepath.Join(workDir, "qemu-netns.pid") 566 resolvPath = filepath.Join(workDir, "qemu-netns-resolv.conf") 567 wrapperPath = filepath.Join(workDir, "qemu-netns-wrapper") 568 569 // the guest resolves through shuttle on 127.0.0.1. keep qemu's slirp DNS 570 // pointed at an unroutable local resolver inside this network namespace so 571 // direct guest queries to 10.0.3.3 don't bypass the shuttle dns policy. 572 if err := os.WriteFile(resolvPath, []byte("nameserver 127.0.0.1\n"), 0o644); err != nil { 573 return "", "", "", fmt.Errorf("write qemu network namespace resolv.conf: %w", err) 574 } 575 576 if err := writeNetnsWrapper(wrapperPath, dev); err != nil { 577 return "", "", "", fmt.Errorf("write qemu network namespace wrapper: %w", err) 578 } 579 580 return pidFile, resolvPath, wrapperPath, nil 581} 582 583type qemuArgsConfig struct { 584 Image ImageSpec 585 CID uint32 586 EnableKVM bool 587 QMPPath string 588 SerialLogPath string 589 VolumePaths map[string]string 590} 591 592func qemuArgs(cfg qemuArgsConfig) ([]string, error) { 593 uuid := uuid.New() 594 595 b := newArgBuilder(64) 596 597 addQEMUMachineArgs(&b, cfg, uuid) 598 addQEMUStoreArgs(&b, cfg) 599 600 if cfg.EnableKVM { 601 addQEMUKVMArgs(&b, cfg.Image) 602 } 603 604 if err := addQEMUVolumeArgs(&b, cfg); err != nil { 605 return nil, err 606 } 607 608 if err := addQEMUNetworkArgs(&b, cfg.Image.NetworkInterfaces); err != nil { 609 return nil, err 610 } 611 612 b.Optf("-device", "vhost-vsock-device,guest-cid=%d", cfg.CID) 613 614 if len(cfg.Image.RunnerConfig.ExtraArgs) > 0 { 615 b.Add(cfg.Image.RunnerConfig.ExtraArgs...) 616 } 617 618 return b.Args(), nil 619} 620 621func addQEMUMachineArgs(b *argBuilder, cfg qemuArgsConfig, uuid uuid.UUID) { 622 if cfg.Image.RunnerConfig.Machine != "" { 623 b.Opt("-M", cfg.Image.RunnerConfig.Machine) 624 } 625 b.Optf("-m", "%dM", cfg.Image.MemoryMiB) 626 b.Opt("-smp", strconv.Itoa(cfg.Image.VCPUs)) 627 628 b.Add( 629 "-nodefaults", 630 "-no-user-config", 631 "-no-reboot", 632 ) 633 634 b.Opt("-kernel", cfg.Image.Kernel) 635 b.Opt("-initrd", cfg.Image.Initrd) 636 637 b.Opt("-device", "virtio-rng-device") 638 639 b.Optf("-smbios", "type=1,uuid=%s", uuid) 640 b.Opt("-serial", "file:"+cfg.SerialLogPath) 641 642 // use virtio console if requsted. this is faster than the serial UART logging 643 // because serial has a higher cost when being accesssed. we still have to 644 // support serial itself for early kernel boot but thats OK. 645 if cfg.Image.RunnerConfig.Console == "hvc0" { 646 b.Optf("-chardev", "file,id=virtiocon0,path=%s,append=on", cfg.SerialLogPath) 647 b.Add("-device", "virtio-serial-device") 648 b.Opt("-device", "virtconsole,chardev=virtiocon0") 649 } 650 b.Opt("-display", "none") 651 b.Opt("-monitor", "none") 652 b.Opt("-append", cfg.Image.BootArgs) 653 654 b.Opt("-sandbox", "on") 655 b.Optf("-qmp", "unix:%s,server,nowait", cfg.QMPPath) 656} 657 658func addQEMUStoreArgs(b *argBuilder, cfg qemuArgsConfig) { 659 drive := newOptionBuilder(8) 660 drive.KV("id", "store") 661 drive.KV("format", "raw") 662 drive.Add("read-only=on") 663 drive.KV("file", cfg.Image.StoreDisk) 664 drive.Add("if=none") 665 drive.Add("aio=io_uring") 666 667 b.Opt("-drive", drive.String()) 668 b.Opt("-device", "virtio-blk-device,drive=store") 669} 670 671func addQEMUKVMArgs(b *argBuilder, image ImageSpec) { 672 b.Flag("-enable-kvm") 673 if image.RunnerConfig.CPU != "" { 674 b.Opt("-cpu", image.RunnerConfig.CPU) 675 } 676 b.Opt("-device", "i8042") 677} 678 679func addQEMUVolumeArgs(b *argBuilder, cfg qemuArgsConfig) error { 680 for index, volume := range cfg.Image.Volumes { 681 path := cfg.VolumePaths[volume.Image] 682 if path == "" { 683 return fmt.Errorf("missing prepared path for volume %q", volume.Image) 684 } 685 686 driveID := fmt.Sprintf("volume%d", index) 687 688 drive := newOptionBuilder(10) 689 drive.KV("id", driveID) 690 drive.KV("format", "raw") 691 drive.Add("read-only=off") 692 drive.KV("file", path) 693 drive.Add("if=none") 694 drive.Add("aio=io_uring") 695 drive.Add("discard=unmap") 696 drive.Add("cache=none") 697 698 b.Opt("-drive", drive.String()) 699 b.Optf("-device", "virtio-blk-device,drive=%s", driveID) 700 } 701 702 return nil 703} 704 705func addQEMUNetworkArgs(b *argBuilder, interfaces []NetworkInterface) error { 706 for _, networkInterface := range interfaces { 707 if networkInterface.Type != "slirp4netns" { 708 return fmt.Errorf("unsupported microvm network interface type %q", networkInterface.Type) 709 } 710 711 netdevOpts := newOptionBuilder(6) 712 netdevOpts.Add("user") 713 netdevOpts.KV("id", networkInterface.ID) 714 netdevOpts.KV("net", innerSlirpNet) 715 netdevOpts.KV("host", innerSlirpHost) 716 netdevOpts.KV("dns", innerSlirpDNS) 717 netdevOpts.KV("dhcpstart", innerSlirpDHCP) 718 719 b.Opt("-netdev", netdevOpts.String()) 720 b.Optf( 721 "-device", "virtio-net-device,netdev=%s,mac=%s", 722 networkInterface.ID, networkInterface.MAC, 723 ) 724 } 725 726 return nil 727} 728 729func waitForPIDFile(ctx context.Context, path string) (string, error) { 730 waitCtx, cancel := context.WithTimeout(ctx, 5*time.Second) 731 defer cancel() 732 733 ticker := time.NewTicker(25 * time.Millisecond) 734 defer ticker.Stop() 735 736 for { 737 data, err := os.ReadFile(path) 738 if err == nil { 739 pid := strings.TrimSpace(string(data)) 740 if pid != "" { 741 return pid, nil 742 } 743 } else if !errors.Is(err, os.ErrNotExist) { 744 return "", fmt.Errorf("read qemu network namespace pid: %w", err) 745 } 746 747 select { 748 case <-waitCtx.Done(): 749 return "", fmt.Errorf("waiting for qemu network namespace pid: %w", waitCtx.Err()) 750 case <-ticker.C: 751 } 752 } 753}