mirror of https://github.com/containers/podman.git
359 lines
9.2 KiB
Go
359 lines
9.2 KiB
Go
//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
|
|
}
|