mirror of
https://github.com/podman-container-tools/podman.git
synced 2026-09-03 06:07:51 +00:00
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>
1445 lines
37 KiB
Go
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
|
|
}
|