spiegel_podman/pkg/machine/qemu/machine.go
Brent Baude e5a4f00b7d Podman 5 machine config file - Step 1
The following PR is the very first step in what will a series of steps
to apply a "common" machine configuration file to all providers.
Function names, method names, struct names, and field names are all up
for debate.  The purpose of this PR is to offer a glimpse at the
direction we intend to take.

This PR also contains temporary structs (i.e. aThing) that are not
exported.  These are merely placeholders.

The configuration work in this PR is also unused of yet.  But the code
is compiled.  Once merged, we can begin the next step of development.

[NO NEW TESTS NEEDED]

Signed-off-by: Brent Baude <bbaude@redhat.com>
2023-12-11 16:26:15 -06:00

1445 lines
37 KiB
Go

//go:build amd64 || arm64
// +build amd64 arm64
package qemu
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io/fs"
"net"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"github.com/containers/common/pkg/config"
gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types"
"github.com/containers/podman/v4/pkg/machine"
"github.com/containers/podman/v4/pkg/machine/define"
"github.com/containers/podman/v4/pkg/machine/qemu/command"
"github.com/containers/podman/v4/pkg/machine/sockets"
"github.com/containers/podman/v4/pkg/machine/vmconfigs"
"github.com/containers/podman/v4/pkg/rootless"
"github.com/containers/podman/v4/pkg/util"
"github.com/containers/storage/pkg/lockfile"
"github.com/digitalocean/go-qemu/qmp"
"github.com/sirupsen/logrus"
)
var (
// vmtype refers to qemu (vs libvirt, krun, etc).
// Could this be moved into Provider
vmtype = machine.QemuVirt
)
const (
VolumeTypeVirtfs = "virtfs"
MountType9p = "9p"
dockerSock = "/var/run/docker.sock"
dockerConnectTimeout = 5 * time.Second
)
// qemuReadyUnit is a unit file that sets up the virtual serial device
// where when the VM is done configuring, it will send an ack
// so a listening host tknows it can begin interacting with it
const qemuReadyUnit = `[Unit]
Requires=dev-virtio\\x2dports-%s.device
After=remove-moby.service sshd.socket sshd.service
After=systemd-user-sessions.service
OnFailure=emergency.target
OnFailureJobMode=isolate
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/sh -c '/usr/bin/echo Ready >/dev/%s'
[Install]
RequiredBy=default.target
`
type MachineVM struct {
// ConfigPath is the path to the configuration file
ConfigPath define.VMFile
// The command line representation of the qemu command
CmdLine command.QemuCmd
// HostUser contains info about host user
vmconfigs.HostUser
// ImageConfig describes the bootable image
machine.ImageConfig
// Mounts is the list of remote filesystems to mount
Mounts []vmconfigs.Mount
// Name of VM
Name string
// PidFilePath is the where the Proxy PID file lives
PidFilePath define.VMFile
// VMPidFilePath is the where the VM PID file lives
VMPidFilePath define.VMFile
// QMPMonitor is the qemu monitor object for sending commands
QMPMonitor command.Monitor
// ReadySocket tells host when vm is booted
ReadySocket define.VMFile
// ResourceConfig is physical attrs of the VM
vmconfigs.ResourceConfig
// SSHConfig for accessing the remote vm
vmconfigs.SSHConfig
// Starting tells us whether the machine is running or if we have just dialed it to start it
Starting bool
// Created contains the original created time instead of querying the file mod time
Created time.Time
// LastUp contains the last recorded uptime
LastUp time.Time
// User at runtime for serializing write operations.
lock *lockfile.LockFile
}
// addMountsToVM converts the volumes passed through the CLI into the specified
// volume driver and adds them to the machine
func (v *MachineVM) addMountsToVM(opts machine.InitOptions) error {
var volumeType string
switch opts.VolumeDriver {
// "" is the default volume driver
case "virtfs", "":
volumeType = VolumeTypeVirtfs
default:
return fmt.Errorf("unknown volume driver: %s", opts.VolumeDriver)
}
mounts := []vmconfigs.Mount{}
for i, volume := range opts.Volumes {
tag := fmt.Sprintf("vol%d", i)
paths := pathsFromVolume(volume)
source := extractSourcePath(paths)
target := extractTargetPath(paths)
readonly, securityModel := extractMountOptions(paths)
if volumeType == VolumeTypeVirtfs {
v.CmdLine.SetVirtfsMount(source, tag, securityModel, readonly)
mounts = append(mounts, vmconfigs.Mount{Type: MountType9p, Tag: tag, Source: source, Target: target, ReadOnly: readonly})
}
}
v.Mounts = mounts
return nil
}
// Init writes the json configuration file to the filesystem for
// other verbs (start, stop)
func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) {
var (
key string
err error
)
// cleanup half-baked files if init fails at any point
callbackFuncs := machine.InitCleanup()
defer callbackFuncs.CleanIfErr(&err)
go callbackFuncs.CleanOnSignal()
v.IdentityPath = util.GetIdentityPath(v.Name)
v.Rootful = opts.Rootful
imagePath, strm, err := machine.Pull(opts.ImagePath, opts.Name, VirtualizationProvider())
if err != nil {
return false, err
}
// By this time, image should be had and uncompressed
callbackFuncs.Add(imagePath.Delete)
// Assign values about the download
v.ImagePath = *imagePath
v.ImageStream = strm.String()
if err = v.addMountsToVM(opts); err != nil {
return false, err
}
v.UID = os.Getuid()
// Add location of bootable image
v.CmdLine.SetBootableImage(v.getImageFile())
if err = machine.AddSSHConnectionsToPodmanSocket(
v.UID,
v.Port,
v.IdentityPath,
v.Name,
v.RemoteUsername,
opts,
); err != nil {
return false, err
}
callbackFuncs.Add(v.removeSystemConnections)
// Write the JSON file
if err = v.writeConfig(); err != nil {
return false, fmt.Errorf("writing JSON file: %w", err)
}
callbackFuncs.Add(v.ConfigPath.Delete)
// User has provided ignition file so keygen
// will be skipped.
if len(opts.IgnitionPath) < 1 {
key, err = machine.CreateSSHKeys(v.IdentityPath)
if err != nil {
return false, err
}
callbackFuncs.Add(v.removeSSHKeys)
}
// Run arch specific things that need to be done
if err = v.prepare(); err != nil {
return false, err
}
originalDiskSize, err := getDiskSize(v.getImageFile())
if err != nil {
return false, err
}
if err = v.resizeDisk(opts.DiskSize, originalDiskSize>>(10*3)); err != nil {
return false, err
}
if opts.UserModeNetworking != nil && !*opts.UserModeNetworking {
logrus.Warn("ignoring init option to disable user-mode networking: this mode is not supported by the QEMU backend")
}
builder := machine.NewIgnitionBuilder(machine.DynamicIgnition{
Name: opts.Username,
Key: key,
VMName: v.Name,
VMType: machine.QemuVirt,
TimeZone: opts.TimeZone,
WritePath: v.getIgnitionFile(),
UID: v.UID,
Rootful: v.Rootful,
})
// If the user provides an ignition file, we need to
// copy it into the conf dir
if len(opts.IgnitionPath) > 0 {
return false, builder.BuildWithIgnitionFile(opts.IgnitionPath)
}
if err := builder.GenerateIgnitionConfig(); err != nil {
return false, err
}
readyUnit := machine.Unit{
Enabled: machine.BoolToPtr(true),
Name: "ready.service",
Contents: machine.StrToPtr(fmt.Sprintf(qemuReadyUnit, "vport1p1", "vport1p1")),
}
builder.WithUnit(readyUnit)
err = builder.Build()
callbackFuncs.Add(v.IgnitionFile.Delete)
return err == nil, err
}
func (v *MachineVM) removeSSHKeys() error {
if err := os.Remove(fmt.Sprintf("%s.pub", v.IdentityPath)); err != nil {
logrus.Error(err)
}
return os.Remove(v.IdentityPath)
}
func (v *MachineVM) removeSystemConnections() error {
return machine.RemoveConnections(v.Name, fmt.Sprintf("%s-root", v.Name))
}
func (v *MachineVM) Set(_ string, opts machine.SetOptions) ([]error, error) {
// If one setting fails to be applied, the others settings will not fail and still be applied.
// The setting(s) that failed to be applied will have its errors returned in setErrors
var setErrors []error
v.lock.Lock()
defer v.lock.Unlock()
state, err := v.State(false)
if err != nil {
return setErrors, err
}
if state == define.Running {
suffix := ""
if v.Name != machine.DefaultMachineName {
suffix = " " + v.Name
}
return setErrors, fmt.Errorf("cannot change settings while the vm is running, run 'podman machine stop%s' first", suffix)
}
if opts.Rootful != nil && v.Rootful != *opts.Rootful {
if err := v.setRootful(*opts.Rootful); err != nil {
setErrors = append(setErrors, fmt.Errorf("failed to set rootful option: %w", err))
} else {
v.Rootful = *opts.Rootful
}
}
if opts.CPUs != nil && v.CPUs != *opts.CPUs {
v.CPUs = *opts.CPUs
v.editCmdLine("-smp", strconv.Itoa(int(v.CPUs)))
}
if opts.Memory != nil && v.Memory != *opts.Memory {
v.Memory = *opts.Memory
v.editCmdLine("-m", strconv.Itoa(int(v.Memory)))
}
if opts.DiskSize != nil && v.DiskSize != *opts.DiskSize {
if err := v.resizeDisk(*opts.DiskSize, v.DiskSize); err != nil {
setErrors = append(setErrors, fmt.Errorf("failed to resize disk: %w", err))
} else {
v.DiskSize = *opts.DiskSize
}
}
if opts.USBs != nil {
if usbConfigs, err := command.ParseUSBs(*opts.USBs); err != nil {
setErrors = append(setErrors, fmt.Errorf("failed to set usb: %w", err))
} else {
v.USBs = usbConfigs
}
}
err = v.writeConfig()
if err != nil {
setErrors = append(setErrors, err)
}
if len(setErrors) > 0 {
return setErrors, setErrors[0]
}
return setErrors, nil
}
// mountVolumesToVM iterates through the machine's volumes and mounts them to the
// machine
func (v *MachineVM) mountVolumesToVM(opts machine.StartOptions, name string) error {
for _, mount := range v.Mounts {
if !opts.Quiet {
fmt.Printf("Mounting volume... %s:%s\n", mount.Source, mount.Target)
}
// create mountpoint directory if it doesn't exist
// because / is immutable, we have to monkey around with permissions
// if we dont mount in /home or /mnt
args := []string{"-q", "--"}
if !strings.HasPrefix(mount.Target, "/home") && !strings.HasPrefix(mount.Target, "/mnt") {
args = append(args, "sudo", "chattr", "-i", "/", ";")
}
args = append(args, "sudo", "mkdir", "-p", mount.Target)
if !strings.HasPrefix(mount.Target, "/home") && !strings.HasPrefix(mount.Target, "/mnt") {
args = append(args, ";", "sudo", "chattr", "+i", "/", ";")
}
err := v.SSH(name, machine.SSHOptions{Args: args})
if err != nil {
return err
}
switch mount.Type {
case MountType9p:
mountOptions := []string{"-t", "9p"}
mountOptions = append(mountOptions, []string{"-o", "trans=virtio", mount.Tag, mount.Target}...)
mountOptions = append(mountOptions, []string{"-o", "version=9p2000.L,msize=131072"}...)
if mount.ReadOnly {
mountOptions = append(mountOptions, []string{"-o", "ro"}...)
}
err = v.SSH(name, machine.SSHOptions{Args: append([]string{"-q", "--", "sudo", "mount"}, mountOptions...)})
if err != nil {
return err
}
default:
return fmt.Errorf("unknown mount type: %s", mount.Type)
}
}
return nil
}
// conductVMReadinessCheck checks to make sure the machine is in the proper state
// and that SSH is up and running
func (v *MachineVM) conductVMReadinessCheck(name string, maxBackoffs int, backoff time.Duration) (connected bool, sshError error, err error) {
for i := 0; i < maxBackoffs; i++ {
if i > 0 {
time.Sleep(backoff)
backoff *= 2
}
state, err := v.State(true)
if err != nil {
return false, nil, err
}
if state == define.Running && v.isListening() {
// Also make sure that SSH is up and running. The
// ready service's dependencies don't fully make sure
// that clients can SSH into the machine immediately
// after boot.
//
// CoreOS users have reported the same observation but
// the underlying source of the issue remains unknown.
if sshError = v.SSH(name, machine.SSHOptions{Args: []string{"true"}}); sshError != nil {
logrus.Debugf("SSH readiness check for machine failed: %v", sshError)
continue
}
connected = true
break
}
}
return
}
// runStartVMCommand executes the command to start the VM
func runStartVMCommand(cmd *exec.Cmd) error {
err := cmd.Start()
if err != nil {
// check if qemu was not found
if !errors.Is(err, os.ErrNotExist) {
return err
}
// look up qemu again maybe the path was changed, https://github.com/containers/podman/issues/13394
cfg, err := config.Default()
if err != nil {
return err
}
qemuBinaryPath, err := cfg.FindHelperBinary(QemuCommand, true)
if err != nil {
return err
}
cmd.Path = qemuBinaryPath
err = cmd.Start()
if err != nil {
return fmt.Errorf("unable to execute %q: %w", cmd, err)
}
}
return nil
}
// qemuPid returns -1 or the PID of the running QEMU instance.
func (v *MachineVM) qemuPid() (int, error) {
pidData, err := os.ReadFile(v.VMPidFilePath.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)
}
// Start executes the qemu command line and forks it
func (v *MachineVM) Start(name string, opts machine.StartOptions) error {
var (
conn net.Conn
err error
qemuSocketConn net.Conn
)
defaultBackoff := 500 * time.Millisecond
maxBackoffs := 6
v.lock.Lock()
defer v.lock.Unlock()
state, err := v.State(false)
if err != nil {
return err
}
switch state {
case define.Starting:
return fmt.Errorf("cannot start VM %q: starting state indicates that a previous start has failed: please stop and restart the VM", v.Name)
case define.Running:
return fmt.Errorf("cannot start VM %q: %w", v.Name, machine.ErrVMAlreadyRunning)
}
// If QEMU is running already, something went wrong and we cannot
// proceed.
qemuPid, err := v.qemuPid()
if err != nil {
return err
}
if qemuPid != -1 {
return fmt.Errorf("cannot start VM %q: another instance of %q is already running with process ID %d: please stop and restart the VM", v.Name, v.CmdLine[0], qemuPid)
}
v.Starting = true
if err := v.writeConfig(); err != nil {
return fmt.Errorf("writing JSON file: %w", err)
}
doneStarting := func() {
v.Starting = false
if err := v.writeConfig(); err != nil {
logrus.Errorf("Writing JSON file: %v", err)
}
}
defer doneStarting()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
_, ok := <-c
if !ok {
return
}
doneStarting()
os.Exit(1)
}()
defer close(c)
if v.isIncompatible() {
logrus.Errorf("machine %q is incompatible with this release of podman and needs to be recreated, starting for recovery only", v.Name)
}
forwardSock, forwardState, err := v.startHostNetworking()
if err != nil {
return fmt.Errorf("unable to start host networking: %q", err)
}
rtPath, err := getRuntimeDir()
if err != nil {
return err
}
// If the temporary podman dir is not created, create it
podmanTempDir := filepath.Join(rtPath, "podman")
if _, err := os.Stat(podmanTempDir); errors.Is(err, fs.ErrNotExist) {
if mkdirErr := os.MkdirAll(podmanTempDir, 0755); mkdirErr != nil {
return err
}
}
// If the qemusocketpath exists and the vm is off/down, we should rm
// it before the dial as to avoid a segv
if err := v.QMPMonitor.Address.Delete(); err != nil {
return err
}
qemuSocketConn, err = sockets.DialSocketWithBackoffs(maxBackoffs, defaultBackoff, v.QMPMonitor.Address.Path)
if err != nil {
return err
}
defer qemuSocketConn.Close()
fd, err := qemuSocketConn.(*net.UnixConn).File()
if err != nil {
return err
}
defer fd.Close()
dnr, dnw, err := machine.GetDevNullFiles()
if err != nil {
return err
}
defer dnr.Close()
defer dnw.Close()
attr := new(os.ProcAttr)
files := []*os.File{dnr, dnw, dnw, fd}
attr.Files = files
cmdLine := v.CmdLine
cmdLine.SetPropagatedHostEnvs()
// Disable graphic window when not in debug mode
// Done in start, so we're not suck with the debug level we used on init
if !logrus.IsLevelEnabled(logrus.DebugLevel) {
cmdLine.SetDisplay("none")
}
logrus.Debugf("qemu cmd: %v", cmdLine)
stderrBuf := &bytes.Buffer{}
// actually run the command that starts the virtual machine
cmd := &exec.Cmd{
Args: cmdLine,
Path: cmdLine[0],
Stdin: dnr,
Stdout: dnw,
Stderr: stderrBuf,
ExtraFiles: []*os.File{fd},
}
if err := runStartVMCommand(cmd); err != nil {
return err
}
defer cmd.Process.Release() //nolint:errcheck
if !opts.Quiet {
fmt.Println("Waiting for VM ...")
}
conn, err = sockets.DialSocketWithBackoffsAndProcCheck(maxBackoffs, defaultBackoff, v.ReadySocket.GetPath(), checkProcessStatus, "qemu", cmd.Process.Pid, stderrBuf)
if err != nil {
return err
}
defer conn.Close()
_, err = bufio.NewReader(conn).ReadString('\n')
if err != nil {
return err
}
// update the podman/docker socket service if the host user has been modified at all (UID or Rootful)
if v.HostUser.Modified {
if machine.UpdatePodmanDockerSockService(v, name, v.UID, v.Rootful) == nil {
// Reset modification state if there are no errors, otherwise ignore errors
// which are already logged
v.HostUser.Modified = false
_ = v.writeConfig()
}
}
if len(v.Mounts) == 0 {
machine.WaitAPIAndPrintInfo(
forwardState,
v.Name,
findClaimHelper(),
forwardSock,
opts.NoInfo,
v.isIncompatible(),
v.Rootful,
)
return nil
}
connected, sshError, err := v.conductVMReadinessCheck(name, maxBackoffs, defaultBackoff)
if err != nil {
return err
}
if !connected {
msg := "machine did not transition into running state"
if sshError != nil {
return fmt.Errorf("%s: ssh error: %v", msg, sshError)
}
return errors.New(msg)
}
// mount the volumes to the VM
if err := v.mountVolumesToVM(opts, name); err != nil {
return err
}
machine.WaitAPIAndPrintInfo(
forwardState,
v.Name,
findClaimHelper(),
forwardSock,
opts.NoInfo,
v.isIncompatible(),
v.Rootful,
)
return nil
}
func (v *MachineVM) 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 (v *MachineVM) waitForMachineToStop() error {
fmt.Println("Waiting for VM to stop running...")
waitInternal := 250 * time.Millisecond
for i := 0; i < 5; i++ {
state, err := v.State(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
}
// ProxyPID retrieves the pid from the proxy pidfile
func (v *MachineVM) ProxyPID() (int, error) {
if _, err := os.Stat(v.PidFilePath.Path); errors.Is(err, fs.ErrNotExist) {
return -1, nil
}
proxyPidString, err := v.PidFilePath.Read()
if err != nil {
return -1, err
}
proxyPid, err := strconv.Atoi(string(proxyPidString))
if err != nil {
return -1, err
}
return proxyPid, nil
}
// cleanupVMProxyProcess kills the proxy process and removes the VM's pidfile
func (v *MachineVM) cleanupVMProxyProcess(proxyProc *os.Process) error {
// Kill the process
if err := proxyProc.Kill(); err != nil {
return err
}
// Remove the pidfile
if err := v.PidFilePath.Delete(); err != nil {
return err
}
return nil
}
// VMPid retrieves the pid from the VM's pidfile
func (v *MachineVM) VMPid() (int, error) {
vmPidString, err := v.VMPidFilePath.Read()
if err != nil {
return -1, err
}
vmPid, err := strconv.Atoi(strings.TrimSpace(string(vmPidString)))
if err != nil {
return -1, err
}
return vmPid, nil
}
// Stop uses the qmp monitor to call a system_powerdown
func (v *MachineVM) Stop(_ string, _ machine.StopOptions) error {
v.lock.Lock()
defer v.lock.Unlock()
if err := v.update(); err != nil {
return err
}
stopErr := v.stopLocked()
// Make sure that the associated QEMU process gets killed in case it's
// still running (#16054).
qemuPid, err := v.qemuPid()
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 (v *MachineVM) stopLocked() error {
// check if the qmp socket is there. if not, qemu instance is gone
if _, err := os.Stat(v.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", v.QMPMonitor.Address)
// Fix incorrect starting state in case of crash during start
if v.Starting {
v.Starting = false
if err := v.writeConfig(); err != nil {
return fmt.Errorf("writing JSON file: %w", err)
}
}
return nil
}
qmpMonitor, err := qmp.NewSocketMonitor(v.QMPMonitor.Network, v.QMPMonitor.Address.GetPath(), v.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
}
proxyPid, err := v.ProxyPID()
if err != nil || proxyPid < 0 {
// may return nil if proxyPid == -1 because the pidfile does not exist
return err
}
proxyProc, err := os.FindProcess(proxyPid)
if proxyProc == nil && err != nil {
return err
}
v.LastUp = time.Now()
if err := v.writeConfig(); err != nil { // keep track of last up
return err
}
if err := v.cleanupVMProxyProcess(proxyProc); err != nil {
return err
}
// Remove socket
if err := v.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 err := v.ReadySocket.Delete(); err != nil {
return err
}
if v.VMPidFilePath.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 v.waitForMachineToStop()
}
vmPid, err := v.VMPid()
if err != nil {
return err
}
fmt.Println("Waiting for VM to exit...")
for isProcessAlive(vmPid) {
time.Sleep(500 * time.Millisecond)
}
return nil
}
// NewQMPMonitor creates the monitor subsection of our vm
func NewQMPMonitor(network, name string, timeout time.Duration) (command.Monitor, error) {
rtDir, err := getRuntimeDir()
if err != nil {
return command.Monitor{}, err
}
if isRootful() {
rtDir = "/run"
}
rtDir = filepath.Join(rtDir, "podman")
if _, err := os.Stat(rtDir); errors.Is(err, fs.ErrNotExist) {
if err := os.MkdirAll(rtDir, 0755); err != nil {
return command.Monitor{}, err
}
}
if timeout == 0 {
timeout = defaultQMPTimeout
}
address, err := define.NewMachineFile(filepath.Join(rtDir, "qmp_"+name+".sock"), nil)
if err != nil {
return command.Monitor{}, err
}
monitor := command.Monitor{
Network: network,
Address: *address,
Timeout: timeout,
}
return monitor, nil
}
// collectFilesToDestroy retrieves the files that will be destroyed by `Remove`
func (v *MachineVM) collectFilesToDestroy(opts machine.RemoveOptions) ([]string, error) {
files := []string{}
// Collect all the files that need to be destroyed
if !opts.SaveKeys {
files = append(files, v.IdentityPath, v.IdentityPath+".pub")
}
if !opts.SaveIgnition {
files = append(files, v.getIgnitionFile())
}
if !opts.SaveImage {
files = append(files, v.getImageFile())
}
socketPath, err := v.forwardSocketPath()
if err != nil {
return nil, err
}
if socketPath.Symlink != nil {
files = append(files, *socketPath.Symlink)
}
files = append(files, socketPath.Path)
files = append(files, v.archRemovalFiles()...)
vmConfigDir, err := machine.GetConfDir(vmtype)
if err != nil {
return nil, err
}
files = append(files, filepath.Join(vmConfigDir, v.Name+".json"))
return files, nil
}
// removeQMPMonitorSocketAndVMPidFile removes the VM pidfile, proxy pidfile,
// and QMP Monitor Socket
func (v *MachineVM) removeQMPMonitorSocketAndVMPidFile() {
// remove socket and pid file if any: warn at low priority if things fail
// Remove the pidfile
if err := v.VMPidFilePath.Delete(); err != nil {
logrus.Debugf("Error while removing VM pidfile: %v", err)
}
if err := v.PidFilePath.Delete(); err != nil {
logrus.Debugf("Error while removing proxy pidfile: %v", err)
}
// Remove socket
if err := v.QMPMonitor.Address.Delete(); err != nil {
logrus.Debugf("Error while removing podman-machine-socket: %v", err)
}
}
// Remove deletes all the files associated with a machine including ssh keys, the image itself
func (v *MachineVM) Remove(_ string, opts machine.RemoveOptions) (string, func() error, error) {
var (
files []string
)
v.lock.Lock()
defer v.lock.Unlock()
// cannot remove a running vm unless --force is used
state, err := v.State(false)
if err != nil {
return "", nil, err
}
if state == define.Running {
if !opts.Force {
return "", nil, &machine.ErrVMRunningCannotDestroyed{Name: v.Name}
}
err := v.stopLocked()
if err != nil {
return "", nil, err
}
}
files, err = v.collectFilesToDestroy(opts)
if err != nil {
return "", nil, err
}
confirmationMessage := "\nThe following files will be deleted:\n\n"
for _, msg := range files {
confirmationMessage += msg + "\n"
}
v.removeQMPMonitorSocketAndVMPidFile()
confirmationMessage += "\n"
return confirmationMessage, func() error {
machine.RemoveFilesAndConnections(files, v.Name, v.Name+"-root")
return nil
}, nil
}
func (v *MachineVM) State(bypass bool) (define.Status, error) {
// Check if qmp socket path exists
if _, err := os.Stat(v.QMPMonitor.Address.GetPath()); errors.Is(err, fs.ErrNotExist) {
return "", nil
}
err := v.update()
if err != nil {
return "", err
}
// Check if we can dial it
if v.Starting && !bypass {
return define.Starting, nil
}
monitor, err := qmp.NewSocketMonitor(v.QMPMonitor.Network, v.QMPMonitor.Address.GetPath(), v.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 {
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 v.checkStatus(monitor)
}
func (v *MachineVM) isListening() bool {
// Check if we can dial it
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", "127.0.0.1", v.Port), 10*time.Millisecond)
if err != nil {
return false
}
conn.Close()
return true
}
// SSH opens an interactive SSH session to the vm specified.
// Added ssh function to VM interface: pkg/machine/config/go : line 58
func (v *MachineVM) SSH(_ string, opts machine.SSHOptions) error {
state, err := v.State(true)
if err != nil {
return err
}
if state != define.Running {
return fmt.Errorf("vm %q is not running", v.Name)
}
username := opts.Username
if username == "" {
username = v.RemoteUsername
}
return machine.CommonSSH(username, v.IdentityPath, v.Name, v.Port, opts.Args)
}
// executes qemu-image info to get the virtual disk size
// of the diskimage
func getDiskSize(path string) (uint64, error) {
// 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
}
// startHostNetworking runs a binary on the host system that allows users
// to set up port forwarding to the podman virtual machine
func (v *MachineVM) startHostNetworking() (string, machine.APIForwardingState, error) {
cfg, err := config.Default()
if err != nil {
return "", machine.NoForwarding, err
}
binary, err := cfg.FindHelperBinary(machine.ForwarderBinaryName, false)
if err != nil {
return "", machine.NoForwarding, err
}
cmd := gvproxy.NewGvproxyCommand()
cmd.AddQemuSocket(fmt.Sprintf("unix://%s", v.QMPMonitor.Address.GetPath()))
cmd.PidFile = v.PidFilePath.GetPath()
cmd.SSHPort = v.Port
var forwardSock string
var state machine.APIForwardingState
if !v.isIncompatible() {
cmd, forwardSock, state = v.setupAPIForwarding(cmd)
}
if logrus.IsLevelEnabled(logrus.DebugLevel) {
cmd.Debug = true
logrus.Debug(cmd)
}
c := cmd.Cmd(binary)
if err := c.Start(); err != nil {
return "", 0, fmt.Errorf("unable to execute: %q: %w", cmd.ToCmdline(), err)
}
return forwardSock, state, nil
}
func (v *MachineVM) setupAPIForwarding(cmd gvproxy.GvproxyCommand) (gvproxy.GvproxyCommand, string, machine.APIForwardingState) {
socket, err := v.forwardSocketPath()
if err != nil {
return cmd, "", machine.NoForwarding
}
destSock := fmt.Sprintf("/run/user/%d/podman/podman.sock", v.UID)
forwardUser := "core"
if v.Rootful {
destSock = "/run/podman/podman.sock"
forwardUser = "root"
}
cmd.AddForwardSock(socket.GetPath())
cmd.AddForwardDest(destSock)
cmd.AddForwardUser(forwardUser)
cmd.AddForwardIdentity(v.IdentityPath)
// The linking pattern is /var/run/docker.sock -> user global sock (link) -> machine sock (socket)
// This allows the helper to only have to maintain one constant target to the user, which can be
// repositioned without updating docker.sock.
link, err := v.userGlobalSocketLink()
if err != nil {
return cmd, socket.GetPath(), machine.MachineLocal
}
if !dockerClaimSupported() {
return cmd, socket.GetPath(), machine.ClaimUnsupported
}
if !dockerClaimHelperInstalled() {
return cmd, socket.GetPath(), machine.NotInstalled
}
if !alreadyLinked(socket.GetPath(), link) {
if checkSockInUse(link) {
return cmd, socket.GetPath(), machine.MachineLocal
}
_ = os.Remove(link)
if err = os.Symlink(socket.GetPath(), link); err != nil {
logrus.Warnf("could not create user global API forwarding link: %s", err.Error())
return cmd, socket.GetPath(), machine.MachineLocal
}
}
if !alreadyLinked(link, dockerSock) {
if checkSockInUse(dockerSock) {
return cmd, socket.GetPath(), machine.MachineLocal
}
if !claimDockerSock() {
logrus.Warn("podman helper is installed, but was not able to claim the global docker sock")
return cmd, socket.GetPath(), machine.MachineLocal
}
}
return cmd, dockerSock, machine.DockerGlobal
}
func (v *MachineVM) isIncompatible() bool {
return v.UID == -1
}
func (v *MachineVM) userGlobalSocketLink() (string, error) {
path, err := machine.GetDataDir(machine.QemuVirt)
if err != nil {
logrus.Errorf("Resolving data dir: %s", err.Error())
return "", err
}
// User global socket is located in parent directory of machine dirs (one per user)
return filepath.Join(filepath.Dir(path), "podman.sock"), err
}
func (v *MachineVM) forwardSocketPath() (*define.VMFile, error) {
sockName := "podman.sock"
path, err := machine.GetDataDir(machine.QemuVirt)
if err != nil {
logrus.Errorf("Resolving data dir: %s", err.Error())
return nil, err
}
return define.NewMachineFile(filepath.Join(path, sockName), &sockName)
}
func (v *MachineVM) setConfigPath() error {
vmConfigDir, err := machine.GetConfDir(vmtype)
if err != nil {
return err
}
configPath, err := define.NewMachineFile(filepath.Join(vmConfigDir, v.Name)+".json", nil)
if err != nil {
return err
}
v.ConfigPath = *configPath
return nil
}
func (v *MachineVM) setPIDSocket() error {
rtPath, err := getRuntimeDir()
if err != nil {
return err
}
if isRootful() {
rtPath = "/run"
}
socketDir := filepath.Join(rtPath, "podman")
vmPidFileName := fmt.Sprintf("%s_vm.pid", v.Name)
proxyPidFileName := fmt.Sprintf("%s_proxy.pid", v.Name)
vmPidFilePath, err := define.NewMachineFile(filepath.Join(socketDir, vmPidFileName), &vmPidFileName)
if err != nil {
return err
}
proxyPidFilePath, err := define.NewMachineFile(filepath.Join(socketDir, proxyPidFileName), &proxyPidFileName)
if err != nil {
return err
}
v.VMPidFilePath = *vmPidFilePath
v.PidFilePath = *proxyPidFilePath
return nil
}
func checkSockInUse(sock string) bool {
if info, err := os.Stat(sock); err == nil && info.Mode()&fs.ModeSocket == fs.ModeSocket {
_, err = net.DialTimeout("unix", dockerSock, dockerConnectTimeout)
return err == nil
}
return false
}
func alreadyLinked(target string, link string) bool {
read, err := os.Readlink(link)
return err == nil && read == target
}
// update returns the content of the VM's
// configuration file in json
func (v *MachineVM) update() error {
if err := v.setConfigPath(); err != nil {
return err
}
b, err := v.ConfigPath.Read()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("%v: %w", v.Name, machine.ErrNoSuchVM)
}
return err
}
if err != nil {
return err
}
return json.Unmarshal(b, v)
}
func (v *MachineVM) writeConfig() error {
// Set the path of the configfile before writing to make
// life easier down the line
if err := v.setConfigPath(); err != nil {
return err
}
// Write the JSON file
return machine.WriteConfig(v.ConfigPath.Path, v)
}
// getImageFile wrapper returns the path to the image used
// to boot the VM
func (v *MachineVM) getImageFile() string {
return v.ImagePath.GetPath()
}
// getIgnitionFile wrapper returns the path to the ignition file
func (v *MachineVM) getIgnitionFile() string {
return v.IgnitionFile.GetPath()
}
// Inspect returns verbose detail about the machine
func (v *MachineVM) Inspect() (*machine.InspectInfo, error) {
state, err := v.State(false)
if err != nil {
return nil, err
}
connInfo := new(machine.ConnectionConfig)
podmanSocket, err := v.forwardSocketPath()
if err != nil {
return nil, err
}
connInfo.PodmanSocket = podmanSocket
return &machine.InspectInfo{
ConfigPath: v.ConfigPath,
ConnectionInfo: *connInfo,
Created: v.Created,
Image: v.ImageConfig,
LastUp: v.LastUp,
Name: v.Name,
Resources: v.ResourceConfig,
SSHConfig: v.SSHConfig,
State: state,
UserModeNetworking: true, // always true
Rootful: v.Rootful,
}, nil
}
// resizeDisk increases the size of the machine's disk in GB.
func (v *MachineVM) resizeDisk(diskSize uint64, oldSize uint64) error {
// Resize the disk image to input disk size
// only if the virtualdisk size is less than
// the given disk size
if diskSize < oldSize {
return fmt.Errorf("new disk size must be larger than current disk size: %vGB", oldSize)
}
// Find the qemu executable
cfg, err := config.Default()
if err != nil {
return err
}
resizePath, err := cfg.FindHelperBinary("qemu-img", true)
if err != nil {
return err
}
resize := exec.Command(resizePath, []string{"resize", v.getImageFile(), strconv.Itoa(int(diskSize)) + "G"}...)
resize.Stdout = os.Stdout
resize.Stderr = os.Stderr
if err := resize.Run(); err != nil {
return fmt.Errorf("resizing image: %q", err)
}
return nil
}
func (v *MachineVM) setRootful(rootful bool) error {
if err := machine.SetRootful(rootful, v.Name, v.Name+"-root"); err != nil {
return err
}
v.HostUser.Modified = true
return nil
}
func (v *MachineVM) editCmdLine(flag string, value string) {
found := false
for i, val := range v.CmdLine {
if val == flag {
found = true
v.CmdLine[i+1] = value
break
}
}
if !found {
v.CmdLine = append(v.CmdLine, []string{flag, value}...)
}
}
func isRootful() bool {
// Rootless is not relevant on Windows. In the future rootless.IsRootless
// could be switched to return true on Windows, and other codepaths migrated
// for now will check additionally for valid os.Getuid
return !rootless.IsRootless() && os.Getuid() != -1
}
func extractSourcePath(paths []string) string {
return paths[0]
}
func extractMountOptions(paths []string) (bool, string) {
readonly := false
securityModel := "none"
if len(paths) > 2 {
options := paths[2]
volopts := strings.Split(options, ",")
for _, o := range volopts {
switch {
case o == "rw":
readonly = false
case o == "ro":
readonly = true
case strings.HasPrefix(o, "security_model="):
securityModel = strings.Split(o, "=")[1]
default:
fmt.Printf("Unknown option: %s\n", o)
}
}
}
return readonly, securityModel
}