diff options
Diffstat (limited to 'pkg/machine')
-rw-r--r-- | pkg/machine/config.go | 7 | ||||
-rw-r--r-- | pkg/machine/config_test.go | 3 | ||||
-rw-r--r-- | pkg/machine/e2e/list_test.go | 23 | ||||
-rw-r--r-- | pkg/machine/fcos.go | 2 | ||||
-rw-r--r-- | pkg/machine/ignition.go | 4 | ||||
-rw-r--r-- | pkg/machine/keys.go | 32 | ||||
-rw-r--r-- | pkg/machine/qemu/config.go | 4 | ||||
-rw-r--r-- | pkg/machine/qemu/config_test.go | 3 | ||||
-rw-r--r-- | pkg/machine/qemu/machine.go | 142 | ||||
-rw-r--r-- | pkg/machine/qemu/machine_test.go | 3 | ||||
-rw-r--r-- | pkg/machine/qemu/options_darwin_arm64.go | 23 | ||||
-rw-r--r-- | pkg/machine/wsl/machine.go | 1 |
12 files changed, 192 insertions, 55 deletions
diff --git a/pkg/machine/config.go b/pkg/machine/config.go index abbebc9f9..fcc129338 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -42,7 +42,9 @@ const ( // Running indicates the qemu vm is running. Running Status = "running" // Stopped indicates the vm has stopped. - Stopped Status = "stopped" + Stopped Status = "stopped" + // Starting indicated the vm is in the process of starting + Starting Status = "starting" DefaultMachineName string = "podman-machine-default" ) @@ -62,7 +64,7 @@ var ( DefaultIgnitionUserName = "core" ErrNoSuchVM = errors.New("VM does not exist") ErrVMAlreadyExists = errors.New("VM already exists") - ErrVMAlreadyRunning = errors.New("VM already running") + ErrVMAlreadyRunning = errors.New("VM already running or starting") ErrMultipleActiveVM = errors.New("only one VM can be active at a time") ForwarderBinaryName = "gvproxy" ) @@ -88,6 +90,7 @@ type ListResponse struct { CreatedAt time.Time LastUp time.Time Running bool + Starting bool Stream string VMType string CPUs uint64 diff --git a/pkg/machine/config_test.go b/pkg/machine/config_test.go index d9fc5425e..ca08660b9 100644 --- a/pkg/machine/config_test.go +++ b/pkg/machine/config_test.go @@ -1,3 +1,6 @@ +//go:build amd64 || arm64 +// +build amd64 arm64 + package machine import ( diff --git a/pkg/machine/e2e/list_test.go b/pkg/machine/e2e/list_test.go index 1c8c6ac81..e2121e7bf 100644 --- a/pkg/machine/e2e/list_test.go +++ b/pkg/machine/e2e/list_test.go @@ -29,7 +29,7 @@ var _ = Describe("podman machine list", func() { firstList, err := mb.setCmd(list).run() Expect(err).NotTo(HaveOccurred()) Expect(firstList).Should(Exit(0)) - Expect(len(firstList.outputToStringSlice())).To(Equal(1)) // just the header + Expect(firstList.outputToStringSlice()).To(HaveLen(1)) // just the header i := new(initMachine) session, err := mb.setCmd(i.withImagePath(mb.imagePath)).run() @@ -39,7 +39,7 @@ var _ = Describe("podman machine list", func() { secondList, err := mb.setCmd(list).run() Expect(err).NotTo(HaveOccurred()) Expect(secondList).To(Exit(0)) - Expect(len(secondList.outputToStringSlice())).To(Equal(2)) // one machine and the header + Expect(secondList.outputToStringSlice()).To(HaveLen(2)) // one machine and the header }) It("list machines with quiet or noheading", func() { @@ -51,12 +51,12 @@ var _ = Describe("podman machine list", func() { firstList, err := mb.setCmd(list.withQuiet()).run() Expect(err).NotTo(HaveOccurred()) Expect(firstList).Should(Exit(0)) - Expect(len(firstList.outputToStringSlice())).To(Equal(0)) // No header with quiet + Expect(firstList.outputToStringSlice()).To(HaveLen(0)) // No header with quiet noheaderSession, err := mb.setCmd(list.withNoHeading()).run() // noheader Expect(err).NotTo(HaveOccurred()) Expect(noheaderSession).Should(Exit(0)) - Expect(len(noheaderSession.outputToStringSlice())).To(Equal(0)) + Expect(noheaderSession.outputToStringSlice()).To(HaveLen(0)) i := new(initMachine) session, err := mb.setName(name1).setCmd(i.withImagePath(mb.imagePath)).run() @@ -70,7 +70,7 @@ var _ = Describe("podman machine list", func() { secondList, err := mb.setCmd(list.withQuiet()).run() Expect(err).NotTo(HaveOccurred()) Expect(secondList).To(Exit(0)) - Expect(len(secondList.outputToStringSlice())).To(Equal(2)) // two machines, no header + Expect(secondList.outputToStringSlice()).To(HaveLen(2)) // two machines, no header listNames := secondList.outputToStringSlice() stripAsterisk(listNames) @@ -116,10 +116,10 @@ var _ = Describe("podman machine list", func() { // go format list := new(listMachine) - listSession, err := mb.setCmd(list.withFormat("{{.Name}}").withNoHeading()).run() + listSession, err := mb.setCmd(list.withFormat("{{.Name}}")).run() Expect(err).NotTo(HaveOccurred()) Expect(listSession).To(Exit(0)) - Expect(len(listSession.outputToStringSlice())).To(Equal(1)) + Expect(listSession.outputToStringSlice()).To(HaveLen(1)) listNames := listSession.outputToStringSlice() stripAsterisk(listNames) @@ -135,6 +135,15 @@ var _ = Describe("podman machine list", func() { var listResponse []*machine.ListReporter err = jsoniter.Unmarshal(listSession.Bytes(), &listResponse) Expect(err).To(BeNil()) + + // table format includes the header + list = new(listMachine) + listSession3, err3 := mb.setCmd(list.withFormat("table {{.Name}}")).run() + Expect(err3).NotTo(HaveOccurred()) + Expect(listSession3).To(Exit(0)) + listNames3 := listSession3.outputToStringSlice() + Expect(listNames3).To(HaveLen(2)) + Expect(listNames3).To(ContainSubstring("NAME")) }) }) diff --git a/pkg/machine/fcos.go b/pkg/machine/fcos.go index 77427139a..59ef6d975 100644 --- a/pkg/machine/fcos.go +++ b/pkg/machine/fcos.go @@ -139,7 +139,7 @@ func getStreamURL(streamType string) url2.URL { // This should get Exported and stay put as it will apply to all fcos downloads // getFCOS parses fedoraCoreOS's stream and returns the image download URL and the release version -func GetFCOSDownload(imageStream string) (*FcosDownloadInfo, error) { //nolint:staticcheck +func GetFCOSDownload(imageStream string) (*FcosDownloadInfo, error) { var ( fcosstable stream.Stream altMeta release.Release diff --git a/pkg/machine/ignition.go b/pkg/machine/ignition.go index 35a9a30cb..f4602cc95 100644 --- a/pkg/machine/ignition.go +++ b/pkg/machine/ignition.go @@ -93,7 +93,7 @@ func NewIgnitionFile(ign DynamicIgnition) error { tz string ) // local means the same as the host - // lookup where it is pointing to on the host + // look up where it is pointing to on the host if ign.TimeZone == "local" { tz, err = getLocalTimeZone() if err != nil { @@ -348,7 +348,7 @@ Delegate=memory pids cpu io }, }) - // Setup /etc/subuid and /etc/subgid + // Set up /etc/subuid and /etc/subgid for _, sub := range []string{"/etc/subuid", "/etc/subgid"} { files = append(files, File{ Node: Node{ diff --git a/pkg/machine/keys.go b/pkg/machine/keys.go index 15c1f73d8..463271427 100644 --- a/pkg/machine/keys.go +++ b/pkg/machine/keys.go @@ -4,14 +4,15 @@ package machine import ( - "errors" "fmt" + "io" "io/ioutil" "os" "os/exec" "path/filepath" "strings" + "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -51,7 +52,23 @@ func CreateSSHKeysPrefix(dir string, file string, passThru bool, skipExisting bo // generatekeys creates an ed25519 set of keys func generatekeys(writeLocation string) error { args := append(append([]string{}, sshCommand[1:]...), writeLocation) - return exec.Command(sshCommand[0], args...).Run() + cmd := exec.Command(sshCommand[0], args...) + stdErr, err := cmd.StderrPipe() + if err != nil { + return err + } + if err := cmd.Start(); err != nil { + return err + } + waitErr := cmd.Wait() + if waitErr == nil { + return nil + } + errMsg, err := io.ReadAll(stdErr) + if err != nil { + return fmt.Errorf("key generation failed, unable to read from stderr: %w", waitErr) + } + return fmt.Errorf("failed to generate keys: %s: %w", string(errMsg), waitErr) } // generatekeys creates an ed25519 set of keys @@ -59,7 +76,16 @@ func generatekeysPrefix(dir string, file string, passThru bool, prefix ...string args := append([]string{}, prefix[1:]...) args = append(args, sshCommand...) args = append(args, file) - cmd := exec.Command(prefix[0], args...) + + binary, err := exec.LookPath(prefix[0]) + if err != nil { + return err + } + binary, err = filepath.Abs(binary) + if err != nil { + return err + } + cmd := exec.Command(binary, args...) cmd.Dir = dir if passThru { cmd.Stdin = os.Stdin diff --git a/pkg/machine/qemu/config.go b/pkg/machine/qemu/config.go index 56c95e3b3..bada1af9b 100644 --- a/pkg/machine/qemu/config.go +++ b/pkg/machine/qemu/config.go @@ -72,8 +72,10 @@ type MachineVM struct { Mounts []machine.Mount // Name of VM Name string - // PidFilePath is the where the PID file lives + // PidFilePath is the where the Proxy PID file lives PidFilePath machine.VMFile + // VMPidFilePath is the where the VM PID file lives + VMPidFilePath machine.VMFile // QMPMonitor is the qemu monitor object for sending commands QMPMonitor Monitor // ReadySocket tells host when vm is booted diff --git a/pkg/machine/qemu/config_test.go b/pkg/machine/qemu/config_test.go index 4d96ec6e7..72cb3ed90 100644 --- a/pkg/machine/qemu/config_test.go +++ b/pkg/machine/qemu/config_test.go @@ -1,3 +1,6 @@ +//go:build (amd64 && !windows) || (arm64 && !windows) +// +build amd64,!windows arm64,!windows + package qemu import ( diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index 0a85ff5ce..7e9c786a9 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -16,9 +16,11 @@ import ( "net/url" "os" "os/exec" + "os/signal" "path/filepath" "strconv" "strings" + "syscall" "time" "github.com/containers/common/pkg/config" @@ -30,6 +32,7 @@ import ( "github.com/docker/go-units" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" ) var ( @@ -105,6 +108,9 @@ func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) { if err != nil { return nil, err } + if err := vm.setPIDSocket(); err != nil { + return nil, err + } cmd := []string{execPath} // Add memory cmd = append(cmd, []string{"-m", strconv.Itoa(int(vm.Memory))}...) @@ -133,11 +139,9 @@ func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) { "-device", "virtio-serial", // qemu needs to establish the long name; other connections can use the symlink'd "-chardev", "socket,path=" + vm.ReadySocket.Path + ",server=on,wait=off,id=" + vm.Name + "_ready", - "-device", "virtserialport,chardev=" + vm.Name + "_ready" + ",name=org.fedoraproject.port.0"}...) + "-device", "virtserialport,chardev=" + vm.Name + "_ready" + ",name=org.fedoraproject.port.0", + "-pidfile", vm.VMPidFilePath.GetPath()}...) vm.CmdLine = cmd - if err := vm.setPIDSocket(); err != nil { - return nil, err - } return vm, nil } @@ -207,7 +211,7 @@ func migrateVM(configPath string, config []byte, vm *MachineVM) error { vm.Rootful = old.Rootful vm.UID = old.UID - // Backup the original config file + // Back up the original config file if err := os.Rename(configPath, configPath+".orig"); err != nil { return err } @@ -484,12 +488,26 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { if err := v.writeConfig(); err != nil { return fmt.Errorf("writing JSON file: %w", err) } - defer func() { + 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) } @@ -564,7 +582,7 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { if !errors.Is(err, os.ErrNotExist) { return err } - // lookup qemu again maybe the path was changed, https://github.com/containers/podman/issues/13394 + // 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 @@ -737,17 +755,17 @@ func (v *MachineVM) Stop(_ string, _ machine.StopOptions) error { if _, err := os.Stat(v.PidFilePath.GetPath()); os.IsNotExist(err) { return nil } - pidString, err := v.PidFilePath.Read() + proxyPidString, err := v.PidFilePath.Read() if err != nil { return err } - pidNum, err := strconv.Atoi(string(pidString)) + proxyPid, err := strconv.Atoi(string(proxyPidString)) if err != nil { return err } - p, err := os.FindProcess(pidNum) - if p == nil && err != nil { + proxyProc, err := os.FindProcess(proxyPid) + if proxyProc == nil && err != nil { return err } @@ -756,7 +774,7 @@ func (v *MachineVM) Stop(_ string, _ machine.StopOptions) error { return err } // Kill the process - if err := p.Kill(); err != nil { + if err := proxyProc.Kill(); err != nil { return err } // Remove the pidfile @@ -770,24 +788,52 @@ func (v *MachineVM) Stop(_ string, _ machine.StopOptions) error { if err := qmpMonitor.Disconnect(); err != nil { // FIXME: this error should probably be returned - return nil // nolint: nilerr + return nil //nolint: nilerr } - disconnected = true - 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 + + 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 + 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 } - 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 } - return v.ReadySocket.Delete() + vmPidString, err := v.VMPidFilePath.Read() + if err != nil { + return err + } + vmPid, err := strconv.Atoi(strings.TrimSpace(string(vmPidString))) + 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 @@ -880,8 +926,11 @@ func (v *MachineVM) Remove(_ string, opts machine.RemoveOptions) (string, func() // 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 pidfile: %v", err) + logrus.Debugf("Error while removing proxy pidfile: %v", err) } // Remove socket if err := v.QMPMonitor.Address.Delete(); err != nil { @@ -910,11 +959,16 @@ func (v *MachineVM) State(bypass bool) (machine.Status, error) { } // Check if we can dial it if v.Starting && !bypass { - return "", nil + return machine.Starting, nil } monitor, err := qmp.NewSocketMonitor(v.QMPMonitor.Network, v.QMPMonitor.Address.GetPath(), v.QMPMonitor.Timeout) if err != nil { - // FIXME: this error should probably be returned + // 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 { @@ -959,7 +1013,7 @@ func (v *MachineVM) SSH(_ string, opts machine.SSHOptions) error { port := strconv.Itoa(v.Port) args := []string{"-i", v.IdentityPath, "-p", port, sshDestination, "-o", "UserKnownHostsFile=/dev/null", - "-o", "StrictHostKeyChecking=no", "-o", "LogLevel=ERROR"} + "-o", "StrictHostKeyChecking=no", "-o", "LogLevel=ERROR", "-o", "SetEnv=LC_ALL="} if len(opts.Args) > 0 { args = append(args, opts.Args...) } else { @@ -1058,6 +1112,7 @@ func getVMInfos() ([]*machine.ListResponse, error) { listEntry.RemoteUsername = vm.RemoteUsername listEntry.IdentityPath = vm.IdentityPath listEntry.CreatedAt = vm.Created + listEntry.Starting = vm.Starting if listEntry.CreatedAt.IsZero() { listEntry.CreatedAt = time.Now() @@ -1071,6 +1126,7 @@ func getVMInfos() ([]*machine.ListResponse, error) { if err != nil { return err } + listEntry.Running = state == machine.Running if !vm.LastUp.IsZero() { // this means we have already written a time to the config listEntry.LastUp = vm.LastUp @@ -1081,9 +1137,6 @@ func getVMInfos() ([]*machine.ListResponse, error) { return err } } - if state == machine.Running { - listEntry.Running = true - } listed = append(listed, listEntry) } @@ -1115,7 +1168,7 @@ func (p *Provider) CheckExclusiveActiveVM() (bool, string, error) { return false, "", errors.Wrap(err, "error checking VM active") } for _, vm := range vms { - if vm.Running { + if vm.Running || vm.Starting { return true, vm.Name, nil } } @@ -1123,7 +1176,7 @@ func (p *Provider) CheckExclusiveActiveVM() (bool, string, error) { } // startHostNetworking runs a binary on the host system that allows users -// to setup port forwarding to the podman virtual machine +// to set up port forwarding to the podman virtual machine func (v *MachineVM) startHostNetworking() (string, apiForwardingState, error) { cfg, err := config.Default() if err != nil { @@ -1290,13 +1343,19 @@ func (v *MachineVM) setPIDSocket() error { if !rootless.IsRootless() { rtPath = "/run" } - pidFileName := fmt.Sprintf("%s.pid", v.Name) socketDir := filepath.Join(rtPath, "podman") - pidFilePath, err := machine.NewMachineFile(filepath.Join(socketDir, pidFileName), &pidFileName) + vmPidFileName := fmt.Sprintf("%s_vm.pid", v.Name) + proxyPidFileName := fmt.Sprintf("%s_proxy.pid", v.Name) + vmPidFilePath, err := machine.NewMachineFile(filepath.Join(socketDir, vmPidFileName), &vmPidFileName) + if err != nil { + return err + } + proxyPidFilePath, err := machine.NewMachineFile(filepath.Join(socketDir, proxyPidFileName), &proxyPidFileName) if err != nil { return err } - v.PidFilePath = *pidFilePath + v.VMPidFilePath = *vmPidFilePath + v.PidFilePath = *proxyPidFilePath return nil } @@ -1633,3 +1692,12 @@ func (p *Provider) RemoveAndCleanMachines() error { } return prevErr } + +func isProcessAlive(pid int) bool { + err := unix.Kill(pid, syscall.Signal(0)) + if err == nil || err == unix.EPERM { + return true + } + + return false +} diff --git a/pkg/machine/qemu/machine_test.go b/pkg/machine/qemu/machine_test.go index 62ca6068a..4c393d0f4 100644 --- a/pkg/machine/qemu/machine_test.go +++ b/pkg/machine/qemu/machine_test.go @@ -1,3 +1,6 @@ +//go:build (amd64 && !windows) || (arm64 && !windows) +// +build amd64,!windows arm64,!windows + package qemu import ( diff --git a/pkg/machine/qemu/options_darwin_arm64.go b/pkg/machine/qemu/options_darwin_arm64.go index 4c954af00..d75237938 100644 --- a/pkg/machine/qemu/options_darwin_arm64.go +++ b/pkg/machine/qemu/options_darwin_arm64.go @@ -4,6 +4,8 @@ import ( "os" "os/exec" "path/filepath" + + "github.com/containers/common/pkg/config" ) var ( @@ -15,8 +17,8 @@ func (v *MachineVM) addArchOptions() []string { opts := []string{ "-accel", "hvf", "-accel", "tcg", - "-cpu", "cortex-a57", - "-M", "virt,highmem=off", + "-cpu", "host", + "-M", "virt,highmem=on", "-drive", "file=" + getEdk2CodeFd("edk2-aarch64-code.fd") + ",if=pflash,format=raw,readonly=on", "-drive", "file=" + ovmfDir + ",if=pflash,format=raw"} return opts @@ -38,6 +40,22 @@ func getOvmfDir(imagePath, vmName string) string { } /* + * When QEmu is installed in a non-default location in the system + * we can use the qemu-system-* binary path to figure the install + * location for Qemu and use it to look for edk2-code-fd + */ +func getEdk2CodeFdPathFromQemuBinaryPath() string { + cfg, err := config.Default() + if err == nil { + execPath, err := cfg.FindHelperBinary(QemuCommand, true) + if err == nil { + return filepath.Clean(filepath.Join(filepath.Dir(execPath), "..", "share", "qemu")) + } + } + return "" +} + +/* * QEmu can be installed in multiple locations on MacOS, especially on * Apple Silicon systems. A build from source will likely install it in * /usr/local/bin, whereas Homebrew package management standard is to @@ -45,6 +63,7 @@ func getOvmfDir(imagePath, vmName string) string { */ func getEdk2CodeFd(name string) string { dirs := []string{ + getEdk2CodeFdPathFromQemuBinaryPath(), "/opt/homebrew/opt/podman/libexec/share/qemu", "/usr/local/share/qemu", "/opt/homebrew/share/qemu", diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index 06020aded..075f42cb2 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -1312,6 +1312,7 @@ func GetVMInfos() ([]*machine.ListResponse, error) { listEntry.RemoteUsername = vm.RemoteUsername listEntry.Port = vm.Port listEntry.IdentityPath = vm.IdentityPath + listEntry.Starting = false running := vm.isRunning() listEntry.CreatedAt, listEntry.LastUp, _ = vm.updateTimeStamps(running) |