Files
seaweedfs/weed/storage/blockvol/test/weed_target.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

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 }