//go:build linux || freebsd package qemu import ( "encoding/json" "errors" "fmt" "io" "io/fs" "os" "os/exec" "strconv" "strings" "syscall" "time" "github.com/containers/common/pkg/config" "github.com/containers/podman/v5/pkg/errorhandling" "github.com/containers/podman/v5/pkg/machine/define" "github.com/containers/podman/v5/pkg/machine/vmconfigs" "github.com/containers/storage/pkg/fileutils" "github.com/digitalocean/go-qemu/qmp" "github.com/sirupsen/logrus" ) const ( MountType9p = "9p" MountTypeVirtiofs = "virtiofs" ) func NewStubber() (*QEMUStubber, error) { var mountType string if v, ok := os.LookupEnv("PODMAN_MACHINE_VIRTFS"); ok { logrus.Debugf("using virtiofs %q", v) switch v { case MountType9p, MountTypeVirtiofs: mountType = v default: return nil, fmt.Errorf("failed to parse PODMAN_MACHINE_VIRTFS=%s", v) } } else { mountType = MountType9p } return &QEMUStubber{ mountType: mountType, }, nil } // qemuPid returns -1 or the PID of the running QEMU instance. func qemuPid(pidFile *define.VMFile) (int, error) { pidData, err := os.ReadFile(pidFile.GetPath()) if err != nil { // The file may not yet exist on start or have already been // cleaned up after stop, so we need to be defensive. if errors.Is(err, os.ErrNotExist) { return -1, nil } return -1, err } if len(pidData) == 0 { return -1, nil } pid, err := strconv.Atoi(strings.TrimRight(string(pidData), "\n")) if err != nil { logrus.Warnf("Reading QEMU pidfile: %v", err) return -1, nil } return findProcess(pid) } // todo move this to qemumonitor stuff. it has no use as a method of stubber func (q *QEMUStubber) checkStatus(monitor *qmp.SocketMonitor) (define.Status, error) { // this is the format returned from the monitor // {"return": {"status": "running", "singlestep": false, "running": true}} type statusDetails struct { Status string `json:"status"` Step bool `json:"singlestep"` Running bool `json:"running"` Starting bool `json:"starting"` } type statusResponse struct { Response statusDetails `json:"return"` } var response statusResponse checkCommand := struct { Execute string `json:"execute"` }{ Execute: "query-status", } input, err := json.Marshal(checkCommand) if err != nil { return "", err } b, err := monitor.Run(input) if err != nil { if errors.Is(err, os.ErrNotExist) { return define.Stopped, nil } return "", err } if err := json.Unmarshal(b, &response); err != nil { return "", err } if response.Response.Status == define.Running { return define.Running, nil } return define.Stopped, nil } // waitForMachineToStop waits for the machine to stop running func (q *QEMUStubber) waitForMachineToStop(mc *vmconfigs.MachineConfig) error { fmt.Println("Waiting for VM to stop running...") waitInternal := 250 * time.Millisecond for i := 0; i < 5; i++ { state, err := q.State(mc, false) if err != nil { return err } if state != define.Running { break } time.Sleep(waitInternal) waitInternal *= 2 } // after the machine stops running it normally takes about 1 second for the // qemu VM to exit so we wait a bit to try to avoid issues time.Sleep(2 * time.Second) return nil } // Stop uses the qmp monitor to call a system_powerdown func (q *QEMUStubber) StopVM(mc *vmconfigs.MachineConfig, _ bool) error { if err := mc.Refresh(); err != nil { return err } stopErr := q.stopLocked(mc) // Make sure that the associated QEMU process gets killed in case it's // still running (#16054). qemuPid, err := qemuPid(mc.QEMUHypervisor.QEMUPidPath) if err != nil { if stopErr == nil { return err } return fmt.Errorf("%w: %w", stopErr, err) } if qemuPid == -1 { return stopErr } if err := sigKill(qemuPid); err != nil { if stopErr == nil { return err } return fmt.Errorf("%w: %w", stopErr, err) } return stopErr } // stopLocked stops the machine and expects the caller to hold the machine's lock. func (q *QEMUStubber) stopLocked(mc *vmconfigs.MachineConfig) error { // check if the qmp socket is there. if not, qemu instance is gone if err := fileutils.Exists(mc.QEMUHypervisor.QMPMonitor.Address.GetPath()); errors.Is(err, fs.ErrNotExist) { // Right now it is NOT an error to stop a stopped machine logrus.Debugf("QMP monitor socket %v does not exist", mc.QEMUHypervisor.QMPMonitor.Address) // Fix incorrect starting state in case of crash during start if mc.Starting { mc.Starting = false if err := mc.Write(); err != nil { return err } } return nil } qmpMonitor, err := qmp.NewSocketMonitor(mc.QEMUHypervisor.QMPMonitor.Network, mc.QEMUHypervisor.QMPMonitor.Address.GetPath(), mc.QEMUHypervisor.QMPMonitor.Timeout) if err != nil { return err } // Simple JSON formation for the QAPI stopCommand := struct { Execute string `json:"execute"` }{ Execute: "system_powerdown", } input, err := json.Marshal(stopCommand) if err != nil { return err } if err := qmpMonitor.Connect(); err != nil { return err } var disconnected bool defer func() { if !disconnected { if err := qmpMonitor.Disconnect(); err != nil { logrus.Error(err) } } }() if _, err = qmpMonitor.Run(input); err != nil { return err } // Remove socket if err := mc.QEMUHypervisor.QMPMonitor.Address.Delete(); err != nil { return err } if err := qmpMonitor.Disconnect(); err != nil { // FIXME: this error should probably be returned return nil //nolint: nilerr } disconnected = true if mc.QEMUHypervisor.QEMUPidPath.GetPath() == "" { // no vm pid file path means it's probably a machine created before we // started using it, so we revert to the old way of waiting for the // machine to stop return q.waitForMachineToStop(mc) } vmPid, err := mc.QEMUHypervisor.QEMUPidPath.ReadPIDFrom() if err != nil { return err } fmt.Println("Waiting for VM to exit...") for isProcessAlive(vmPid) { time.Sleep(500 * time.Millisecond) } return nil } // Remove deletes all the files associated with a machine including the image itself func (q *QEMUStubber) Remove(mc *vmconfigs.MachineConfig) ([]string, func() error, error) { qemuRmFiles := []string{ mc.QEMUHypervisor.QEMUPidPath.GetPath(), mc.QEMUHypervisor.QMPMonitor.Address.GetPath(), } return qemuRmFiles, func() error { var errs []error if err := mc.QEMUHypervisor.QEMUPidPath.Delete(); err != nil { errs = append(errs, err) } if err := mc.QEMUHypervisor.QMPMonitor.Address.Delete(); err != nil { errs = append(errs, err) } return errorhandling.JoinErrors(errs) }, nil } func (q *QEMUStubber) State(mc *vmconfigs.MachineConfig, bypass bool) (define.Status, error) { // Check if qmp socket path exists if err := fileutils.Exists(mc.QEMUHypervisor.QMPMonitor.Address.GetPath()); errors.Is(err, fs.ErrNotExist) { return define.Stopped, nil } if err := mc.Refresh(); err != nil { return "", err } // TODO this has always been a problem, lets fix this // Check if we can dial it // if v.Starting && !bypass { // return define.Starting, nil // } monitor, err := qmp.NewSocketMonitor(mc.QEMUHypervisor.QMPMonitor.Network, mc.QEMUHypervisor.QMPMonitor.Address.GetPath(), mc.QEMUHypervisor.QMPMonitor.Timeout) if err != nil { // If an improper cleanup was done and the socketmonitor was not deleted, // it can appear as though the machine state is not stopped. Check for ECONNREFUSED // almost assures us that the vm is stopped. if errors.Is(err, syscall.ECONNREFUSED) { return define.Stopped, nil } return "", err } if err := monitor.Connect(); err != nil { // There is a case where if we stop the same vm (from running) two // consecutive times we can get an econnreset when trying to get the // state if errors.Is(err, syscall.ECONNRESET) { // try again logrus.Debug("received ECCONNRESET from QEMU monitor; trying again") secondTry := monitor.Connect() if errors.Is(secondTry, io.EOF) { return define.Stopped, nil } if secondTry != nil { logrus.Debugf("second attempt to connect to QEMU monitor failed") return "", secondTry } } return "", err } defer func() { if err := monitor.Disconnect(); err != nil { logrus.Error(err) } }() // If there is a monitor, let's see if we can query state return q.checkStatus(monitor) } // executes qemu-image info to get the virtual disk size // of the diskimage func getDiskSize(path string) (uint64, error) { //nolint:unused // Find the qemu executable cfg, err := config.Default() if err != nil { return 0, err } qemuPathDir, err := cfg.FindHelperBinary("qemu-img", true) if err != nil { return 0, err } diskInfo := exec.Command(qemuPathDir, "info", "--output", "json", path) stdout, err := diskInfo.StdoutPipe() if err != nil { return 0, err } if err := diskInfo.Start(); err != nil { return 0, err } tmpInfo := struct { VirtualSize uint64 `json:"virtual-size"` Filename string `json:"filename"` ClusterSize int64 `json:"cluster-size"` Format string `json:"format"` FormatSpecific struct { Type string `json:"type"` Data map[string]string `json:"data"` } DirtyFlag bool `json:"dirty-flag"` }{} if err := json.NewDecoder(stdout).Decode(&tmpInfo); err != nil { return 0, err } if err := diskInfo.Wait(); err != nil { return 0, err } return tmpInfo.VirtualSize, nil }