mirror of
https://github.com/containers/podman.git
synced 2025-05-21 00:56:36 +08:00
1589 lines
41 KiB
Go
1589 lines
41 KiB
Go
//go:build amd64 || arm64
|
|
// +build amd64 arm64
|
|
|
|
package qemu
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/base64"
|
|
"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/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 QemuCmd
|
|
// HostUser contains info about host user
|
|
machine.HostUser
|
|
// ImageConfig describes the bootable image
|
|
machine.ImageConfig
|
|
// Mounts is the list of remote filesystems to mount
|
|
Mounts []machine.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 Monitor
|
|
// ReadySocket tells host when vm is booted
|
|
ReadySocket define.VMFile
|
|
// ResourceConfig is physical attrs of the VM
|
|
machine.ResourceConfig
|
|
// SSHConfig for accessing the remote vm
|
|
machine.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
|
|
}
|
|
|
|
type Monitor struct {
|
|
// Address portion of the qmp monitor (/tmp/tmp.sock)
|
|
Address define.VMFile
|
|
// Network portion of the qmp monitor (unix)
|
|
Network string
|
|
// Timeout in seconds for qmp monitor transactions
|
|
Timeout time.Duration
|
|
}
|
|
|
|
// migrateVM takes the old configuration structure and migrates it
|
|
// to the new structure and writes it to the filesystem
|
|
func migrateVM(configPath string, config []byte, vm *MachineVM) error {
|
|
fmt.Printf("Migrating machine %q\n", vm.Name)
|
|
var old MachineVMV1
|
|
err := json.Unmarshal(config, &old)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Looks like we loaded the older structure; now we need to migrate
|
|
// from the old structure to the new structure
|
|
_, pidFile, err := vm.getSocketandPid()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pidFilePath := define.VMFile{Path: pidFile}
|
|
qmpMonitor := Monitor{
|
|
Address: define.VMFile{Path: old.QMPMonitor.Address},
|
|
Network: old.QMPMonitor.Network,
|
|
Timeout: old.QMPMonitor.Timeout,
|
|
}
|
|
socketPath, err := getRuntimeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
virtualSocketPath := filepath.Join(socketPath, "podman", vm.Name+"_ready.sock")
|
|
readySocket := define.VMFile{Path: virtualSocketPath}
|
|
|
|
vm.HostUser = machine.HostUser{}
|
|
vm.ImageConfig = machine.ImageConfig{}
|
|
vm.ResourceConfig = machine.ResourceConfig{}
|
|
vm.SSHConfig = machine.SSHConfig{}
|
|
|
|
ignitionFilePath, err := define.NewMachineFile(old.IgnitionFilePath, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
imagePath, err := define.NewMachineFile(old.ImagePath, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// setReadySocket will stick the entry into the new struct
|
|
symlink := vm.Name + "_ready.sock"
|
|
if err := machine.SetSocket(&vm.ReadySocket, machine.ReadySocketPath(socketPath+"/podman/", vm.Name), &symlink); err != nil {
|
|
return err
|
|
}
|
|
|
|
vm.CPUs = old.CPUs
|
|
vm.CmdLine = old.CmdLine
|
|
vm.DiskSize = old.DiskSize
|
|
vm.IdentityPath = old.IdentityPath
|
|
vm.IgnitionFile = *ignitionFilePath
|
|
vm.ImagePath = *imagePath
|
|
vm.ImageStream = old.ImageStream
|
|
vm.Memory = old.Memory
|
|
vm.Mounts = old.Mounts
|
|
vm.Name = old.Name
|
|
vm.PidFilePath = pidFilePath
|
|
vm.Port = old.Port
|
|
vm.QMPMonitor = qmpMonitor
|
|
vm.ReadySocket = readySocket
|
|
vm.RemoteUsername = old.RemoteUsername
|
|
vm.Rootful = old.Rootful
|
|
vm.UID = old.UID
|
|
|
|
// Back up the original config file
|
|
if err := os.Rename(configPath, configPath+".orig"); err != nil {
|
|
return err
|
|
}
|
|
// Write the config file
|
|
if err := vm.writeConfig(); err != nil {
|
|
// If the config file fails to be written, put the original
|
|
// config file back before erroring
|
|
if renameError := os.Rename(configPath+".orig", configPath); renameError != nil {
|
|
logrus.Warn(renameError)
|
|
}
|
|
return err
|
|
}
|
|
// Remove the backup file
|
|
return os.Remove(configPath + ".orig")
|
|
}
|
|
|
|
// 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 := []machine.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, machine.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 == machine.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 := 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 == machine.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 machine.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 machine.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 = machine.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 = machine.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
|
|
}
|
|
|
|
// propagateHostEnv is here for providing the ability to propagate
|
|
// proxy and SSL settings (e.g. HTTP_PROXY and others) on a start
|
|
// and avoid a need of re-creating/re-initiating a VM
|
|
func propagateHostEnv(cmdLine QemuCmd) QemuCmd {
|
|
varsToPropagate := make([]string, 0)
|
|
|
|
for k, v := range machine.GetProxyVariables() {
|
|
varsToPropagate = append(varsToPropagate, fmt.Sprintf("%s=%q", k, v))
|
|
}
|
|
|
|
if sslCertFile, ok := os.LookupEnv("SSL_CERT_FILE"); ok {
|
|
pathInVM := filepath.Join(machine.UserCertsTargetPath, filepath.Base(sslCertFile))
|
|
varsToPropagate = append(varsToPropagate, fmt.Sprintf("%s=%q", "SSL_CERT_FILE", pathInVM))
|
|
}
|
|
|
|
if _, ok := os.LookupEnv("SSL_CERT_DIR"); ok {
|
|
varsToPropagate = append(varsToPropagate, fmt.Sprintf("%s=%q", "SSL_CERT_DIR", machine.UserCertsTargetPath))
|
|
}
|
|
|
|
if len(varsToPropagate) > 0 {
|
|
prefix := "name=opt/com.coreos/environment,string="
|
|
envVarsJoined := strings.Join(varsToPropagate, "|")
|
|
fwCfgArg := prefix + base64.StdEncoding.EncodeToString([]byte(envVarsJoined))
|
|
return append(cmdLine, "-fw_cfg", fwCfgArg)
|
|
}
|
|
|
|
return cmdLine
|
|
}
|
|
|
|
func (v *MachineVM) checkStatus(monitor *qmp.SocketMonitor) (machine.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 machine.Stopped, nil
|
|
}
|
|
return "", err
|
|
}
|
|
if err := json.Unmarshal(b, &response); err != nil {
|
|
return "", err
|
|
}
|
|
if response.Response.Status == machine.Running {
|
|
return machine.Running, nil
|
|
}
|
|
return machine.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 != machine.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) (Monitor, error) {
|
|
rtDir, err := getRuntimeDir()
|
|
if err != nil {
|
|
return 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 Monitor{}, err
|
|
}
|
|
}
|
|
if timeout == 0 {
|
|
timeout = defaultQMPTimeout
|
|
}
|
|
address, err := define.NewMachineFile(filepath.Join(rtDir, "qmp_"+name+".sock"), nil)
|
|
if err != nil {
|
|
return Monitor{}, err
|
|
}
|
|
monitor := 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 == machine.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) (machine.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 machine.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 machine.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 != machine.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
|
|
}
|
|
|
|
// Deprecated: getSocketandPid is being replaced by setPIDSocket and
|
|
// machinefiles.
|
|
func (v *MachineVM) getSocketandPid() (string, string, error) {
|
|
rtPath, err := getRuntimeDir()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if isRootful() {
|
|
rtPath = "/run"
|
|
}
|
|
socketDir := filepath.Join(rtPath, "podman")
|
|
pidFile := filepath.Join(socketDir, fmt.Sprintf("%s.pid", v.Name))
|
|
qemuSocket := filepath.Join(socketDir, fmt.Sprintf("qemu_%s.sock", v.Name))
|
|
return qemuSocket, pidFile, 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
|
|
}
|
|
err = json.Unmarshal(b, v)
|
|
if err != nil {
|
|
err = migrateVM(v.ConfigPath.GetPath(), b, v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
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
|
|
}
|