mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
102 lines
3.2 KiB
Go
102 lines
3.2 KiB
Go
//go:build e2e
|
|
|
|
package harness
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
)
|
|
|
|
// GuestSSHEndpoint resolves (host, 22) for SSH-ing into a guest VM. No
|
|
// qemu-hostfwd fallback: hostfwd bypasses the OVN datapath (see
|
|
// InstancePublicSSHHost). A running instance with no public IP is a hard failure.
|
|
func GuestSSHEndpoint(t *testing.T, c *AWSClient, cluster *Cluster, instanceID string, opts ...PollOpt) (host string, port int) {
|
|
t.Helper()
|
|
_ = cluster // retained for signature stability; node hostfwd lookup removed
|
|
inst := WaitForInstanceState(t, c, instanceID, "running", opts...)
|
|
pub := aws.StringValue(inst.PublicIpAddress)
|
|
if pub == "" {
|
|
t.Fatalf("GuestSSHEndpoint: instance %s has no public IP; "+
|
|
"qemu-hostfwd fallback is disabled (it bypasses the OVN datapath)", instanceID)
|
|
}
|
|
return pub, 22
|
|
}
|
|
|
|
// GuestSSHReady polls SSH into the named instance until a probe command
|
|
// succeeds. Pemfile is the test-scoped private key written by EnsureKeyPair;
|
|
// user is the AMI's stock default user (e.g. ubuntu for Ubuntu cloud images),
|
|
// which stock cloud-init wires the IMDS launch key to once the seed is retired.
|
|
func GuestSSHReady(t *testing.T, host string, port int, user, pemfile string, opts ...PollOpt) {
|
|
t.Helper()
|
|
cfg := applyOpts(pollCfg{timeout: 60 * time.Second, interval: 1 * time.Second}, opts...)
|
|
target := fmt.Sprintf("%s@%s", user, host)
|
|
|
|
EventuallyErr(t, func() error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), cfg.interval+2*time.Second)
|
|
defer cancel()
|
|
args := []string{
|
|
"-i", pemfile,
|
|
"-p", strconv.Itoa(port),
|
|
"-o", "StrictHostKeyChecking=no",
|
|
"-o", "UserKnownHostsFile=/dev/null",
|
|
"-o", "ConnectTimeout=2",
|
|
"-o", "BatchMode=yes",
|
|
"-o", "LogLevel=ERROR",
|
|
target,
|
|
"echo ready",
|
|
}
|
|
if err := runSSH(ctx, args); err != nil {
|
|
return fmt.Errorf("ssh %s:%d: %w", host, port, err)
|
|
}
|
|
return nil
|
|
}, cfg.timeout, cfg.interval)
|
|
}
|
|
|
|
// TryGuestSSHReady is the non-fatal twin of GuestSSHReady: it polls SSH up to
|
|
// timeout and returns whether the probe ever succeeded instead of t.Fatal-ing.
|
|
// Callers use a false return to dump datapath diagnostics before failing — a
|
|
// fresh cross-node public-IP guest unreachable for the full window is the
|
|
// SNAT/DNAT flow-install flake signature, only diagnosable from CI artifacts.
|
|
func TryGuestSSHReady(host string, port int, user, pemfile string, timeout time.Duration) bool {
|
|
target := fmt.Sprintf("%s@%s", user, host)
|
|
args := []string{
|
|
"-i", pemfile,
|
|
"-p", strconv.Itoa(port),
|
|
"-o", "StrictHostKeyChecking=no",
|
|
"-o", "UserKnownHostsFile=/dev/null",
|
|
"-o", "ConnectTimeout=2",
|
|
"-o", "BatchMode=yes",
|
|
"-o", "LogLevel=ERROR",
|
|
target,
|
|
"echo ready",
|
|
}
|
|
deadline := time.Now().Add(timeout)
|
|
for {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
|
|
err := runSSH(ctx, args)
|
|
cancel()
|
|
if err == nil {
|
|
return true
|
|
}
|
|
if time.Now().After(deadline) {
|
|
return false
|
|
}
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
}
|
|
|
|
func runSSH(ctx context.Context, args []string) error {
|
|
out, err := exec.CommandContext(ctx, "ssh", args...).CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(out)))
|
|
}
|
|
return nil
|
|
}
|