Files
seaweedfs/weed/storage/blockvol/test/integration_test.go
T
Ping QiuandClaude Opus 4.6 44da35faf6 test: add integration test infrastructure for blockvol iSCSI
Test harness for running blockvol iSCSI tests on WSL2 and remote nodes
(m01/M02). Includes Node (SSH/local exec), ISCSIClient (discover/login/
logout), WeedTarget (weed volume server lifecycle), and test suites for
smoke, stress, crash recovery, chaos, perf benchmarks, and apps (fio/dd).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:05:29 -08:00

282 lines
7.2 KiB
Go

//go:build integration
package test
import (
"context"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
var (
flagEnv = flag.String("env", "wsl2", "wsl2 or remote")
flagTargetHost = flag.String("target-host", "127.0.0.1", "target node IP (SSH)")
flagClientHost = flag.String("client-host", "127.0.0.1", "initiator node IP (SSH)")
flagISCSIHost = flag.String("iscsi-host", "", "iSCSI target IP for discovery/login (defaults to target-host)")
flagSSHKey = flag.String("ssh-key", "", "SSH private key path")
flagSSHUser = flag.String("ssh-user", "testdev", "SSH user")
flagRepoDir = flag.String("repo-dir", "C:/work/seaweedfs", "seaweedfs repo path")
)
// Global state shared across tests.
var (
targetNode *Node
clientNode *Node
artifacts *ArtifactCollector
)
const iqnPrefix = "iqn.2024.com.seaweedfs:test"
func TestMain(m *testing.M) {
flag.Parse()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
// Setup nodes
if *flagEnv == "wsl2" {
targetNode = &Node{IsLocal: true}
clientNode = targetNode // same node for WSL2
} else {
targetNode = &Node{Host: *flagTargetHost, User: *flagSSHUser, KeyFile: *flagSSHKey}
clientNode = &Node{Host: *flagClientHost, User: *flagSSHUser, KeyFile: *flagSSHKey}
}
if err := targetNode.Connect(); err != nil {
fmt.Fprintf(os.Stderr, "FATAL: target connect: %v\n", err)
os.Exit(1)
}
if clientNode != targetNode {
if err := clientNode.Connect(); err != nil {
fmt.Fprintf(os.Stderr, "FATAL: client connect: %v\n", err)
os.Exit(1)
}
}
// Preflight: print versions
preflight(ctx)
// Build target binary
fmt.Println("=== Building iscsi-target binary ===")
tgt := NewTarget(targetNode, DefaultTargetConfig())
if err := tgt.Build(ctx, *flagRepoDir); err != nil {
fmt.Fprintf(os.Stderr, "FATAL: build target: %v\n", err)
os.Exit(1)
}
if err := tgt.Deploy(*flagRepoDir + "/iscsi-target-linux"); err != nil {
fmt.Fprintf(os.Stderr, "FATAL: deploy target: %v\n", err)
os.Exit(1)
}
fmt.Println("=== Build + deploy complete ===")
// Setup artifact collector (no Target -- each test provides its own)
artDir, _ := filepath.Abs("artifacts")
artifacts = NewArtifactCollector(artDir, clientNode)
// Run tests
code := m.Run()
// Global cleanup (unconditional)
cleanup()
os.Exit(code)
}
func preflight(ctx context.Context) {
fmt.Println("=== Preflight ===")
checks := []struct {
name string
cmd string
node *Node
}{
{"fio", "fio --version", clientNode},
{"iscsiadm", "iscsiadm --version 2>&1", clientNode},
{"go", "go version", targetNode},
{"kernel", "uname -r", targetNode},
}
for _, c := range checks {
stdout, _, code, err := c.node.Run(ctx, c.cmd)
if err != nil || code != 0 {
fmt.Printf(" %-10s MISSING\n", c.name)
} else {
fmt.Printf(" %-10s %s\n", c.name, firstLine(stdout))
}
}
fmt.Println("=== End Preflight ===")
}
func cleanup() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
fmt.Println("=== Global Cleanup ===")
iscsi := NewISCSIClient(clientNode)
iscsi.CleanupAll(ctx, iqnPrefix)
// Unmount any test mount points
clientNode.RunRoot(ctx, "umount -f /tmp/blockvol-mnt 2>/dev/null")
// Kill any leftover target process
targetNode.Run(ctx, "pkill -f iscsi-target-test 2>/dev/null")
// Remove temp files
targetNode.Run(ctx, "rm -f /tmp/blockvol-test.blk /tmp/blockvol-test.blk.wal /tmp/iscsi-target-test /tmp/iscsi-target-test.log")
if clientNode != targetNode {
clientNode.Close()
}
targetNode.Close()
fmt.Println("=== Cleanup Done ===")
}
// TestHarnessSelfCheck validates the test framework itself.
// Run first: go test -tags integration -run TestHarnessSelfCheck -v
func TestHarnessSelfCheck(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
cfg := DefaultTargetConfig()
cfg.IQN = iqnPrefix + "-harness"
cfg.VolSize = "50M"
tgt := NewTarget(targetNode, cfg)
iscsi := NewISCSIClient(clientNode)
host := targetHost()
t.Cleanup(func() {
iscsi.Logout(ctx, cfg.IQN)
tgt.Stop(ctx)
tgt.Cleanup(ctx)
})
t.Cleanup(func() { artifacts.Collect(t, tgt) })
// Start target
t.Log("starting target...")
if err := tgt.Start(ctx, true); err != nil {
t.Fatalf("start target: %v", err)
}
// Discovery
t.Log("discovering...")
iqns, err := iscsi.Discover(ctx, host, cfg.Port)
if err != nil {
t.Fatalf("discover: %v", err)
}
found := false
for _, iqn := range iqns {
if iqn == cfg.IQN {
found = true
}
}
if !found {
t.Fatalf("IQN %s not in discovery: %v", cfg.IQN, iqns)
}
// Login
t.Log("logging in...")
dev, err := iscsi.Login(ctx, cfg.IQN)
if err != nil {
t.Fatalf("login: %v", err)
}
t.Logf("device: %s", dev)
// DD 1MB write + read + verify
t.Log("dd write/read verify...")
_, _, code, err := clientNode.RunRoot(ctx,
fmt.Sprintf("dd if=/dev/urandom of=%s bs=1M count=1 oflag=direct 2>/dev/null", dev))
if err != nil || code != 0 {
t.Fatalf("dd write failed: code=%d err=%v", code, err)
}
wSum, _, _, _ := clientNode.RunRoot(ctx,
fmt.Sprintf("dd if=%s bs=1M count=1 iflag=direct 2>/dev/null | md5sum", dev))
t.Logf("md5: %s", firstLine(wSum))
// Logout
t.Log("logging out...")
if err := iscsi.Logout(ctx, cfg.IQN); err != nil {
t.Fatalf("logout: %v", err)
}
// Stop target
t.Log("stopping target...")
if err := tgt.Stop(ctx); err != nil {
t.Fatalf("stop: %v", err)
}
t.Log("harness self-check passed")
}
// targetHost returns the iSCSI target address for discovery/login from the initiator.
// Uses -iscsi-host if set, otherwise falls back to -target-host.
func targetHost() string {
if *flagEnv == "wsl2" {
return "127.0.0.1"
}
if *flagISCSIHost != "" {
return *flagISCSIHost
}
return *flagTargetHost
}
func firstLine(s string) string {
for i, c := range s {
if c == '\n' || c == '\r' {
return s[:i]
}
}
return s
}
// newTestTarget creates a target with test-specific IQN, unique vol file, and cleanup.
// Tests must not run in parallel -- they share the same target node and port.
func newTestTarget(t *testing.T, volSize, walSize string) (*Target, *ISCSIClient, string) {
cfg := DefaultTargetConfig()
// Sanitize test name for IQN -- replace / with - (subtests use /)
name := strings.ReplaceAll(t.Name(), "/", "-")
cfg.IQN = iqnPrefix + "-" + strings.ToLower(name)
if volSize != "" {
cfg.VolSize = volSize
}
if walSize != "" {
cfg.WALSize = walSize
}
tgt := NewTarget(targetNode, cfg)
iscsi := NewISCSIClient(clientNode)
host := targetHost()
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
iscsi.Logout(ctx, cfg.IQN)
tgt.Stop(ctx)
tgt.Cleanup(ctx)
})
t.Cleanup(func() { artifacts.Collect(t, tgt) })
return tgt, iscsi, host
}
// startAndLogin creates volume, starts target, discovers, logs in, returns device.
func startAndLogin(t *testing.T, ctx context.Context, tgt *Target, iscsi *ISCSIClient, host string) string {
t.Helper()
if err := tgt.Start(ctx, true); err != nil {
t.Fatalf("start target: %v", err)
}
if _, err := iscsi.Discover(ctx, host, tgt.config.Port); err != nil {
t.Fatalf("discover: %v", err)
}
dev, err := iscsi.Login(ctx, tgt.config.IQN)
if err != nil {
t.Fatalf("login: %v", err)
}
return dev
}