mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-05 22:12:04 +02:00
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>
213 lines
5.8 KiB
Go
213 lines
5.8 KiB
Go
//go:build integration
|
|
|
|
package test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// WeedTarget manages the lifecycle of a `weed volume --block.listen` process.
|
|
// Unlike Target (standalone iscsi-target binary), this builds and runs the
|
|
// full weed binary with block volume support.
|
|
type WeedTarget struct {
|
|
node *Node
|
|
config TargetConfig
|
|
binPath string // remote path to weed binary
|
|
pid int
|
|
logFile string
|
|
blockDir string // remote dir containing .blk files
|
|
volFile string // remote path to the .blk file
|
|
iqnPrefix string
|
|
}
|
|
|
|
// NewWeedTarget creates a WeedTarget bound to a node.
|
|
func NewWeedTarget(node *Node, config TargetConfig) *WeedTarget {
|
|
return &WeedTarget{
|
|
node: node,
|
|
config: config,
|
|
binPath: "/tmp/weed-test",
|
|
logFile: "/tmp/weed-test.log",
|
|
blockDir: "/tmp/blockvol-weedtest",
|
|
iqnPrefix: "iqn.2024-01.com.seaweedfs:vol.",
|
|
}
|
|
}
|
|
|
|
// Build cross-compiles the weed binary for linux/amd64.
|
|
func (t *WeedTarget) Build(ctx context.Context, repoDir string) error {
|
|
binDir := repoDir + "/weed"
|
|
outPath := repoDir + "/weed-linux"
|
|
|
|
cmd := exec.CommandContext(ctx, "go", "build", "-o", outPath, ".")
|
|
cmd.Dir = binDir
|
|
cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=0")
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("build weed failed: %s\n%w", out, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Deploy uploads the pre-built weed binary to the target node.
|
|
func (t *WeedTarget) Deploy(localBin string) error {
|
|
return t.node.Upload(localBin, t.binPath)
|
|
}
|
|
|
|
// Start launches `weed volume --block.listen`. If create is true, creates
|
|
// the block directory and volume file first.
|
|
func (t *WeedTarget) Start(ctx context.Context, create bool) error {
|
|
// Remove old log
|
|
t.node.Run(ctx, fmt.Sprintf("rm -f %s", t.logFile))
|
|
|
|
if create {
|
|
// Create block directory and volume file
|
|
t.node.Run(ctx, fmt.Sprintf("rm -rf %s", t.blockDir))
|
|
t.node.Run(ctx, fmt.Sprintf("mkdir -p %s", t.blockDir))
|
|
|
|
// Derive volume name from IQN suffix
|
|
volName := t.volName()
|
|
t.volFile = fmt.Sprintf("%s/%s.blk", t.blockDir, volName)
|
|
|
|
// Create the .blk file (truncate to size)
|
|
_, _, code, err := t.node.Run(ctx,
|
|
fmt.Sprintf("truncate -s %s %s", t.config.VolSize, t.volFile))
|
|
if err != nil || code != 0 {
|
|
return fmt.Errorf("create volume file: code=%d err=%v", code, err)
|
|
}
|
|
}
|
|
|
|
// Start weed volume with block support
|
|
args := fmt.Sprintf("volume -port=19333 -block.listen=:%d -block.dir=%s",
|
|
t.config.Port, t.blockDir)
|
|
|
|
cmd := fmt.Sprintf("setsid -f %s %s >%s 2>&1", t.binPath, args, t.logFile)
|
|
_, stderr, code, err := t.node.Run(ctx, cmd)
|
|
if err != nil || code != 0 {
|
|
return fmt.Errorf("start weed volume: code=%d stderr=%s err=%v", code, stderr, err)
|
|
}
|
|
|
|
// Wait for iSCSI port
|
|
if err := t.WaitForPort(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Discover PID
|
|
stdout, _, _, _ := t.node.Run(ctx,
|
|
fmt.Sprintf("ps -eo pid,args | grep '%s' | grep -v grep | awk '{print $1}'", t.binPath))
|
|
pidStr := strings.TrimSpace(stdout)
|
|
if idx := strings.IndexByte(pidStr, '\n'); idx > 0 {
|
|
pidStr = pidStr[:idx]
|
|
}
|
|
pid, err := strconv.Atoi(pidStr)
|
|
if err != nil {
|
|
return fmt.Errorf("find weed PID: %q: %w", pidStr, err)
|
|
}
|
|
t.pid = pid
|
|
return nil
|
|
}
|
|
|
|
// Stop sends SIGTERM, waits up to 10s, then Kill9.
|
|
func (t *WeedTarget) Stop(ctx context.Context) error {
|
|
if t.pid == 0 {
|
|
return nil
|
|
}
|
|
|
|
t.node.Run(ctx, fmt.Sprintf("kill %d", t.pid))
|
|
|
|
deadline := time.Now().Add(10 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
_, _, code, _ := t.node.Run(ctx, fmt.Sprintf("kill -0 %d 2>/dev/null", t.pid))
|
|
if code != 0 {
|
|
t.pid = 0
|
|
return nil
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
|
|
return t.Kill9()
|
|
}
|
|
|
|
// Kill9 sends SIGKILL immediately.
|
|
func (t *WeedTarget) Kill9() error {
|
|
if t.pid == 0 {
|
|
return nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
t.node.Run(ctx, fmt.Sprintf("kill -9 %d", t.pid))
|
|
t.pid = 0
|
|
return nil
|
|
}
|
|
|
|
// Restart stops and starts weed volume (preserving the volume file).
|
|
func (t *WeedTarget) Restart(ctx context.Context) error {
|
|
if err := t.Stop(ctx); err != nil {
|
|
return fmt.Errorf("restart stop: %w", err)
|
|
}
|
|
return t.Start(ctx, false)
|
|
}
|
|
|
|
// WaitForPort polls until the iSCSI port is listening.
|
|
func (t *WeedTarget) WaitForPort(ctx context.Context) error {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("wait for port %d: %w", t.config.Port, ctx.Err())
|
|
default:
|
|
}
|
|
|
|
stdout, _, code, _ := t.node.Run(ctx, fmt.Sprintf("ss -tln | grep :%d", t.config.Port))
|
|
if code == 0 && strings.Contains(stdout, fmt.Sprintf(":%d", t.config.Port)) {
|
|
return nil
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// CollectLog downloads the log file contents.
|
|
func (t *WeedTarget) CollectLog() (string, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
stdout, _, _, err := t.node.Run(ctx, fmt.Sprintf("cat %s 2>/dev/null", t.logFile))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return stdout, nil
|
|
}
|
|
|
|
// Cleanup removes the block directory, volume files, and log.
|
|
func (t *WeedTarget) Cleanup(ctx context.Context) {
|
|
t.node.Run(ctx, fmt.Sprintf("rm -rf %s %s", t.blockDir, t.logFile))
|
|
}
|
|
|
|
// IQN returns the expected IQN for the volume.
|
|
func (t *WeedTarget) IQN() string {
|
|
return t.iqnPrefix + t.volName()
|
|
}
|
|
|
|
// volName derives the volume name from the config IQN or a default.
|
|
func (t *WeedTarget) volName() string {
|
|
// Use IQN suffix if set, otherwise "test"
|
|
if t.config.IQN != "" {
|
|
parts := strings.Split(t.config.IQN, ":")
|
|
if len(parts) > 1 {
|
|
return parts[len(parts)-1]
|
|
}
|
|
}
|
|
return "test"
|
|
}
|
|
|
|
// PID returns the current process ID.
|
|
func (t *WeedTarget) PID() int { return t.pid }
|
|
|
|
// VolFilePath returns the remote volume file path.
|
|
func (t *WeedTarget) VolFilePath() string { return t.volFile }
|
|
|
|
// LogFile returns the remote log file path.
|
|
func (t *WeedTarget) LogFile() string { return t.logFile }
|