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