mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
Two-phase prepare/commit/cancel protocol ensures all replicas expand atomically. Standalone volumes use direct-commit (unchanged behavior). Engine: PrepareExpand/CommitExpand/CancelExpand with on-disk PreparedSize+ExpandEpoch in superblock, crash recovery clears stale prepare state on open, v.mu serializes concurrent expand operations. Proto: 3 new RPCs (PrepareExpand/CommitExpand/CancelExpandBlockVolume). Coordinator: expandClean flag pattern — ReleaseExpandInflight only on clean success or full cancel. Partial replica commit failure calls MarkExpandFailed (keeps ExpandInProgress=true, suppresses heartbeat size updates). ClearExpandFailed for manual reconciliation. Registry: AcquireExpandInflight records PendingExpandSize+ExpandEpoch. ExpandFailed state blocks new expands until cleared. Tests: 15 engine + 4 VS + 10 coordinator + heartbeat suppression regression + updated QA CP82/durability tests with prepare/commit mocks. Also includes CP11A-1 remaining: QA storage profile tests, QA io_backend config tests, testrunner perf-baseline scenarios and coordinated-expand actions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
190 lines
5.1 KiB
Go
190 lines
5.1 KiB
Go
package actions
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
|
|
tr "github.com/seaweedfs/seaweedfs/weed/storage/blockvol/testrunner"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol/testrunner/infra"
|
|
)
|
|
|
|
// RegisterISCSIActions registers iSCSI client actions.
|
|
func RegisterISCSIActions(r *tr.Registry) {
|
|
r.RegisterFunc("iscsi_login", tr.TierBlock, iscsiLogin)
|
|
r.RegisterFunc("iscsi_login_direct", tr.TierBlock, iscsiLoginDirect)
|
|
r.RegisterFunc("iscsi_logout", tr.TierBlock, iscsiLogout)
|
|
r.RegisterFunc("iscsi_discover", tr.TierBlock, iscsiDiscover)
|
|
r.RegisterFunc("iscsi_cleanup", tr.TierBlock, iscsiCleanup)
|
|
}
|
|
|
|
// iscsiLogin discovers + logs into the target, returns the device path.
|
|
func iscsiLogin(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
targetName := act.Target
|
|
if targetName == "" {
|
|
return nil, fmt.Errorf("iscsi_login: target is required")
|
|
}
|
|
|
|
spec, ok := actx.Scenario.Targets[targetName]
|
|
if !ok {
|
|
return nil, fmt.Errorf("iscsi_login: target %q not in scenario", targetName)
|
|
}
|
|
|
|
host, err := getTargetHost(actx, targetName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get the initiator node (first available or explicit).
|
|
node, err := getNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("iscsi_login: %w", err)
|
|
}
|
|
|
|
client := infra.NewISCSIClient(node)
|
|
iqn := spec.IQN()
|
|
port := spec.ISCSIPort
|
|
|
|
actx.Log(" discovering %s:%d ...", host, port)
|
|
iqns, err := client.Discover(ctx, host, port)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("iscsi_login discover: %w", err)
|
|
}
|
|
|
|
// Find matching IQN.
|
|
found := false
|
|
for _, q := range iqns {
|
|
if q == iqn {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return nil, fmt.Errorf("iscsi_login: IQN %s not found in discovery (got %v)", iqn, iqns)
|
|
}
|
|
|
|
actx.Log(" logging in to %s ...", iqn)
|
|
dev, err := client.Login(ctx, iqn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("iscsi_login: %w", err)
|
|
}
|
|
|
|
actx.Log(" device: %s", dev)
|
|
return map[string]string{"value": dev}, nil
|
|
}
|
|
|
|
// iscsiLoginDirect discovers + logs into a target using explicit host, port, iqn params.
|
|
// Unlike iscsi_login, it does not require a target spec — useful for cluster-provisioned
|
|
// volumes whose iSCSI address comes from the master API response.
|
|
func iscsiLoginDirect(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
host := act.Params["host"]
|
|
if host == "" {
|
|
return nil, fmt.Errorf("iscsi_login_direct: host param required")
|
|
}
|
|
portStr := act.Params["port"]
|
|
if portStr == "" {
|
|
return nil, fmt.Errorf("iscsi_login_direct: port param required")
|
|
}
|
|
port, err := strconv.Atoi(portStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("iscsi_login_direct: invalid port %q: %w", portStr, err)
|
|
}
|
|
iqn := act.Params["iqn"]
|
|
if iqn == "" {
|
|
return nil, fmt.Errorf("iscsi_login_direct: iqn param required")
|
|
}
|
|
|
|
node, err := getNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("iscsi_login_direct: %w", err)
|
|
}
|
|
|
|
client := infra.NewISCSIClient(node)
|
|
|
|
actx.Log(" discovering %s:%d ...", host, port)
|
|
iqns, derr := client.Discover(ctx, host, port)
|
|
if derr != nil {
|
|
return nil, fmt.Errorf("iscsi_login_direct discover: %w", derr)
|
|
}
|
|
|
|
found := false
|
|
for _, q := range iqns {
|
|
if q == iqn {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return nil, fmt.Errorf("iscsi_login_direct: IQN %s not found in discovery (got %v)", iqn, iqns)
|
|
}
|
|
|
|
actx.Log(" logging in to %s ...", iqn)
|
|
dev, lerr := client.Login(ctx, iqn)
|
|
if lerr != nil {
|
|
return nil, fmt.Errorf("iscsi_login_direct: %w", lerr)
|
|
}
|
|
|
|
actx.Log(" device: %s", dev)
|
|
return map[string]string{"value": dev}, nil
|
|
}
|
|
|
|
func iscsiLogout(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
targetName := act.Target
|
|
if targetName == "" {
|
|
return nil, fmt.Errorf("iscsi_logout: target is required")
|
|
}
|
|
|
|
spec, ok := actx.Scenario.Targets[targetName]
|
|
if !ok {
|
|
return nil, fmt.Errorf("iscsi_logout: target %q not in scenario", targetName)
|
|
}
|
|
|
|
node, err := getNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("iscsi_logout: %w", err)
|
|
}
|
|
|
|
client := infra.NewISCSIClient(node)
|
|
return nil, client.Logout(ctx, spec.IQN())
|
|
}
|
|
|
|
func iscsiDiscover(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
targetName := act.Target
|
|
if targetName == "" {
|
|
return nil, fmt.Errorf("iscsi_discover: target is required")
|
|
}
|
|
|
|
spec, ok := actx.Scenario.Targets[targetName]
|
|
if !ok {
|
|
return nil, fmt.Errorf("iscsi_discover: target %q not in scenario", targetName)
|
|
}
|
|
|
|
host, err := getTargetHost(actx, targetName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
node, err := getNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("iscsi_discover: %w", err)
|
|
}
|
|
|
|
client := infra.NewISCSIClient(node)
|
|
iqns, err := client.Discover(ctx, host, spec.ISCSIPort)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return map[string]string{"value": fmt.Sprintf("%v", iqns)}, nil
|
|
}
|
|
|
|
func iscsiCleanup(ctx context.Context, actx *tr.ActionContext, act tr.Action) (map[string]string, error) {
|
|
node, err := getNode(actx, act.Node)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("iscsi_cleanup: %w", err)
|
|
}
|
|
|
|
client := infra.NewISCSIClient(node)
|
|
return nil, client.CleanupAll(ctx, "iqn.2024-01.com.seaweedfs:")
|
|
}
|