Files
Rajkaran Yadav 1e80ec74f5 machine: timeout built-in SSH command checks
Add a 30-second context timeout to the VM readiness SSH check in
conductVMReadinessCheck(). This prevents indefinite blocking when the SSH
server hangs (e.g., due to OpenSSH 10.2p1 interoperability issues).
LocalhostSSH and LocalhostSSHSilent remain uncapped to preserve indefinite
execution for legitimate long-running SSH commands like ansible-playbook
or Hyper-V client9p.

Add stale Starting-state recovery at the beginning of startLocked() to clear
mc.Starting=true when the provider reports the machine is Stopped or Running.
This recovers from hard process termination where deferred cleanup cannot execute.
The recovery only clears Starting when State() returns Stopped or Running,
preserving it for Starting, Unknown, and error cases to respect provider state
semantics.

Adds an E2E test that simulates persisted stale Starting=true state and
verifies that the next podman machine start recovers the state and starts
the machine successfully.

Fixes: #29880
Signed-off-by: Rajkaran Yadav <yadavrajkaran854@gmail.com>
2026-10-09 02:29:48 +05:30

208 lines
6.9 KiB
Go

package machine
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
// LocalhostSSH is a common function for ssh'ing to a podman machine using system-connections
// and a port
// TODO This should probably be taught about an machineconfig to reduce input
func LocalhostSSH(username, identityPath, name string, sshPort int, inputArgs []string) error {
return localhostBuiltinSSH(username, identityPath, name, sshPort, inputArgs, true, os.Stdin)
}
// LocalhostSSHShellForceTerm runs the native ssh shell client and forces a terminal (-t)
func LocalhostSSHShellForceTerm(username, identityPath, name string, sshPort int, inputArgs []string) error {
return localhostNativeSSH(username, identityPath, name, sshPort, inputArgs, os.Stdin, true)
}
func LocalhostSSHShell(username, identityPath, name string, sshPort int, inputArgs []string) error {
return localhostNativeSSH(username, identityPath, name, sshPort, inputArgs, os.Stdin, false)
}
func LocalhostSSHSilent(username, identityPath, name string, sshPort int, inputArgs []string) error {
return localhostBuiltinSSH(username, identityPath, name, sshPort, inputArgs, false, nil)
}
func LocalhostSSHWithStdin(username, identityPath, name string, sshPort int, inputArgs []string, stdin io.Reader) error {
return localhostBuiltinSSH(username, identityPath, name, sshPort, inputArgs, true, stdin)
}
// LocalhostSSHWithCtx is like LocalhostSSHSilent but accepts a context for cancellation.
// It is intended for operations that need to be cancellable (e.g., readiness checks)
// and should not be used for long-running commands where cancellation is not desired.
func LocalhostSSHWithCtx(ctx context.Context, username, identityPath, name string, sshPort int, inputArgs []string) error {
return localhostBuiltinSSHWithCtx(ctx, username, identityPath, name, sshPort, inputArgs, false, nil)
}
// LocalhostSSHCopy uses scp to copy files from/to a localhost machine using ssh.
func LocalhostSSHCopy(username, identityPath string, sshPort int, srcPath, destPath string, isSrcFromGuest, quiet bool) error {
var src, dest string
if isSrcFromGuest {
src = username + "@localhost:" + srcPath
dest = destPath
} else {
src = srcPath
dest = username + "@localhost:" + destPath
}
args := append(
LocalhostSSHArgs(), // Warning: This MUST NOT be generalized to allow communication over untrusted networks.
"-r",
"-i", identityPath,
"-P", strconv.Itoa(sshPort),
src, dest)
cmd := exec.Command("scp", args...)
if !quiet {
cmd.Stdout = os.Stdout
}
cmd.Stderr = os.Stderr
return cmd.Run()
}
type sshDebugLogger struct{}
func (w *sshDebugLogger) Write(p []byte) (int, error) {
logrus.Debugf("ssh output: %s", string(p))
return len(p), nil
}
func localhostBuiltinSSH(username, identityPath, name string, sshPort int, inputArgs []string, passOutput bool, stdin io.Reader) error {
return localhostBuiltinSSHWithCtx(context.Background(), username, identityPath, name, sshPort, inputArgs, passOutput, stdin)
}
func localhostBuiltinSSHWithCtx(ctx context.Context, username, identityPath, name string, sshPort int, inputArgs []string, passOutput bool, stdin io.Reader) error {
config, err := createLocalhostConfig(username, identityPath) // WARNING: This MUST NOT be generalized to allow communication over untrusted networks.
if err != nil {
return err
}
client, err := ssh.Dial("tcp", fmt.Sprintf("localhost:%d", sshPort), config)
if err != nil {
return err
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
return err
}
defer session.Close()
cmd := strings.Join(inputArgs, " ")
logrus.Debugf("Running ssh command on machine %q: %s", name, cmd)
session.Stdin = stdin
if passOutput {
session.Stdout = os.Stdout
session.Stderr = os.Stderr
} else if logrus.IsLevelEnabled(logrus.DebugLevel) {
logger := &sshDebugLogger{}
session.Stdout = logger
session.Stderr = logger
}
// If context has a deadline, wrap session.Run to respect it
if _, ok := ctx.Deadline(); ok {
resultChan := make(chan error, 1)
go func() {
resultChan <- session.Run(cmd)
}()
select {
case err := <-resultChan:
return err
case <-ctx.Done():
// Context cancelled - close session to unblock the goroutine
session.Close()
return fmt.Errorf("ssh command %q on machine %q cancelled: %w", cmd, name, ctx.Err())
}
}
return session.Run(cmd)
}
// createLocalhostConfig returns a *ssh.ClientConfig for authenticating a user using a private key
//
// WARNING: This MUST NOT be used to communicate over untrusted networks.
func createLocalhostConfig(user string, identityPath string) (*ssh.ClientConfig, error) {
key, err := os.ReadFile(identityPath)
if err != nil {
return nil, err
}
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return nil, err
}
return &ssh.ClientConfig{
// Not specifying ciphers / MACs seems to allow fairly weak ciphers. This config is restricted
// to connecting to localhost: where we rely on the kernel's process isolation, not primarily on cryptography.
User: user,
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
// This config is restricted to connecting to localhost (and to a VM we manage),
// we rely on the kernel's process isolation, not on cryptography,
// This would be UNACCEPTABLE for most other uses.
}, nil
}
func localhostNativeSSH(username, identityPath, name string, sshPort int, inputArgs []string, stdin io.Reader, forceTerm bool) error {
sshDestination := username + "@localhost"
port := strconv.Itoa(sshPort)
interactive := true
args := append(LocalhostSSHArgs(), // Warning: This MUST NOT be generalized to allow communication over untrusted networks.
"-i", identityPath,
"-p", port,
sshDestination)
if len(inputArgs) > 0 {
// on the other condition, the term is forced
// anyway
if forceTerm {
args = append(args, "-t")
}
interactive = false
args = append(args, inputArgs...)
} else {
// ensure we have a tty
args = append(args, "-t")
fmt.Printf("Connecting to vm %s. To close connection, use `~.` or `exit`\n", name)
}
cmd := exec.Command("ssh", args...)
logrus.Debugf("Executing: ssh %v\n", args)
if err := setupIOPassthrough(cmd, interactive, stdin); err != nil {
return err
}
return cmd.Run()
}
// LocalhostSSHArgs returns OpenSSH command-line options for connecting with no host key identity checks.
//
// WARNING: This MUST NOT be used to communicate over untrusted networks.
func LocalhostSSHArgs() []string {
// This config is restricted to connecting to localhost (and to a VM we manage),
// we rely on the kernel's process isolation, not on cryptography,
// This would be UNACCEPTABLE for most other uses.
return []string{
"-o", "IdentitiesOnly=yes",
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=" + os.DevNull,
"-o", "CheckHostIP=no",
"-o", "LogLevel=ERROR",
"-o", "SetEnv=LC_ALL=",
}
}