Skip to content

testops: add shared runner platform - #48

Draft
pingqiu wants to merge 6 commits into
mainfrom
testops/rdma-nixl-bed
Draft

testops: add shared runner platform#48
pingqiu wants to merge 6 commits into
mainfrom
testops/rdma-nixl-bed

Conversation

@pingqiu

@pingqiu pingqiu commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

What changed

Adds the shared TestOps runner platform under testops/ in seaweedfs/artifactory.

This brings in:

  • the Go scenario runner core;
  • per-product command entry points (sweedrdma, sweeds3, swblock, etc.);
  • RDMA/S3/VFS/block packs and scenario metadata;
  • the controller/dashboard code and QA bundle assertion profiles;
  • the RDMA unified lab gate scenario;
  • a NIXL provider CPU gate wrapper scenario (scenarios/nixl-provider-cpu-gate.yaml);
  • a TestOps GitHub Actions workflow for Go tests, vet, RDMA/NIXL scenario validation, and docs build.

It also adds docs/nixl-kvcache-testbed.md, which defines the next testbed path for NIXL object compatibility, RDMA hardening, and a CPU in-memory KVCache MVP.

Why

The product code should stay in seaweed-mono, but the shared test controller, suite registry, scenario metadata, QA profiles, and evidence tooling need a stable public home. artifactory/testops becomes that home.

This lets RDMA/NIXL/KVCache work use the same TestOps bundle contract and dashboard flow instead of relying on private/local runner copies.

NIXL scope

The NIXL addition is intentionally thin. TestOps does not implement NIXL or RDMA data movement. It runs the mono-side NIXL provider gate and parses stable witness lines such as:

  • NIXL_PROVIDER_GET_GATE_PASS
  • NIXL_PROVIDER_READ_BENCH_RESULT
  • NIXL_PROVIDER_PUT_GATE_PASS
  • NIXL_PROVIDER_CPU_GATE_PASS

The current NIXL gate is CPU provider only. It does not claim GPU/cuObject/VRAM support.

Validation

Run locally from testops/:

  • go test ./...
  • go vet ./...
  • go run ./cmd/sweedrdma validate scenarios/rdma-unified-lab-gate.yaml
  • go run ./cmd/sweedrdma validate scenarios/nixl-provider-cpu-gate.yaml
  • python -m mkdocs build --strict
  • git diff --check

GitHub Actions on this PR:

  • Go test + docs passed.

No result bundles, generated docs site, or binaries are included.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dcef4cfd-d568-44b4-921b-340948d3f9fe

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch testops/rdma-nixl-bed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the TestOps framework, a YAML-driven scenario runner for storage-product validation, including various product-agnostic actions, product-specific packs, a web dashboard, and a queue controller. The review feedback highlights several critical correctness and robustness issues, including a potential command injection vulnerability in the cleanup actions, compatibility and silent failure issues in the PostgreSQL benchmark initialization script, code duplication of shell quoting functions, potential nil pointer panics in volume field extraction, and hanging commands in git/hostname helpers due to missing timeouts.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

if p == "" {
continue
}
node.RunRoot(ctx, fmt.Sprintf("pkill -9 %s 2>/dev/null || true", p))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Potential Command Injection / Missing Shell Quoting

The p parameter (derived from kill_patterns) is directly interpolated into the shell command without any quoting or sanitization. If a pattern contains spaces or shell metacharacters, it could lead to command injection or command failure.

Use the shellQuoteCleanup helper function (defined on line 157 of this file) to safely quote the argument before executing it.

Suggested change
node.RunRoot(ctx, fmt.Sprintf("pkill -9 %s 2>/dev/null || true", p))
node.RunRoot(ctx, fmt.Sprintf("pkill -9 %s 2>/dev/null || true", shellQuoteCleanup(p)))

Comment on lines +158 to +209
pgBin := paramDefault(act.Params, "pg_bin", "/usr/lib/postgresql/16/bin")

node, err := GetNode(actx, act.Node)
if err != nil {
return nil, err
}

pgdata := mount + "/pgdata"

// Format, mount, init PostgreSQL, start, create bench DB, run pgbench -i.
script := fmt.Sprintf(`set -e
# Stop any previous instance
sudo -u postgres %s/pg_ctl -D %s stop 2>/dev/null || true
sleep 1
# Format and mount
mkfs.%s -F %s > /dev/null 2>&1
mkdir -p %s
mount %s %s
# Init PostgreSQL
mkdir -p %s
chown postgres:postgres %s
sudo -u postgres %s/initdb -D %s > /dev/null 2>&1
echo "listen_addresses = '127.0.0.1'" >> %s/postgresql.conf
echo "port = %s" >> %s/postgresql.conf
echo "unix_socket_directories = '/tmp'" >> %s/postgresql.conf
echo "shared_buffers = 256MB" >> %s/postgresql.conf
echo "effective_cache_size = 512MB" >> %s/postgresql.conf
echo "work_mem = 4MB" >> %s/postgresql.conf
echo "wal_buffers = 16MB" >> %s/postgresql.conf
echo "max_connections = 200" >> %s/postgresql.conf
chown -R postgres:postgres %s
# Start
sudo -u postgres %s/pg_ctl -D %s -l %s/logfile start
sleep 3
# Create DB and init pgbench
sudo -u postgres %s/createdb -h /tmp -p %s benchdb 2>/dev/null || true
sudo -u postgres pgbench -h /tmp -i -s %s -p %s benchdb 2>&1 | tail -3
echo PGBENCH_INIT_OK`,
pgBin, pgdata,
fstype, device,
mount,
device, mount,
pgdata,
pgdata,
pgBin, pgdata,
pgdata, port, pgdata, pgdata,
pgdata, pgdata, pgdata, pgdata, pgdata,
pgdata,
pgBin, pgdata, pgdata,
pgBin, port,
scale, port,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Robustness and Correctness Issues in pgbenchInit Script

There are several critical issues in the current shell script template for pgbenchInit:

  1. mkfs.xfs Compatibility: mkfs.xfs does not support the -F flag (it uses -f to force overwrite). If fstype is xfs, the command will fail.
  2. Silent Failures: Because set -o pipefail is not enabled, the exit status of the pgbench ... | tail -3 pipeline is determined solely by tail -3 (which always succeeds). Any initialization failure in pgbench will be silently ignored, and the script will incorrectly report success.
  3. Robustness of Re-initialization: If the device is already mounted or busy from a previous run, mkfs will fail. It is safer to explicitly unmount the device and mountpoint first using umount -l (lazy unmount) to ensure a clean state.
	pgBin := paramDefault(act.Params, "pg_bin", "/usr/lib/postgresql/16/bin")

	node, err := GetNode(actx, act.Node)
	if err != nil {
		return nil, err
	}

	pgdata := mount + "/pgdata"

	forceFlag := "-F"
	if fstype == "xfs" {
		forceFlag = "-f"
	}

	// Format, mount, init PostgreSQL, start, create bench DB, run pgbench -i.
	script := fmt.Sprintf(`set -eo pipefail
# Stop any previous instance
sudo -u postgres %s/pg_ctl -D %s stop 2>/dev/null || true
sleep 1
# Ensure unmounted and clean
sudo umount -l %s 2>/dev/null || true
sudo umount -l %s 2>/dev/null || true
# Format and mount
mkfs.%s %s %s > /dev/null 2>&1
mkdir -p %s
mount %s %s
# Init PostgreSQL
mkdir -p %s
chown postgres:postgres %s
sudo -u postgres %s/initdb -D %s > /dev/null 2>&1
echo "listen_addresses = '127.0.0.1'" >> %s/postgresql.conf
echo "port = %s" >> %s/postgresql.conf
echo "unix_socket_directories = '/tmp'" >> %s/postgresql.conf
echo "shared_buffers = 256MB" >> %s/postgresql.conf
echo "effective_cache_size = 512MB" >> %s/postgresql.conf
echo "work_mem = 4MB" >> %s/postgresql.conf
echo "wal_buffers = 16MB" >> %s/postgresql.conf
echo "max_connections = 200" >> %s/postgresql.conf
chown -R postgres:postgres %s
# Start
sudo -u postgres %s/pg_ctl -D %s -l %s/logfile start
sleep 3
# Create DB and init pgbench
sudo -u postgres %s/createdb -h /tmp -p %s benchdb 2>/dev/null || true
sudo -u postgres pgbench -h /tmp -i -s %s -p %s benchdb 2>&1 | tail -3
echo PGBENCH_INIT_OK`,
		pgBin, pgdata,
		device, mount,
		fstype, forceFlag, device,
		mount,
		device, mount,
		pgdata,
		pgdata,
		pgBin, pgdata,
		pgdata, port, pgdata, pgdata,
		pgdata, pgdata, pgdata, pgdata, pgdata,
		pgdata,
		pgBin, pgdata, pgdata,
		pgBin, port,
		scale, port,
	)

Comment thread testops/actions/system.go
// execShellQuote single-quote escapes a value for POSIX shell. Empty
// becomes an empty quoted string; otherwise wraps in single quotes and escapes any
// embedded single-quote by closing-escaping-reopening.
func execShellQuote(s string) string {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Code Duplication of Shell Quoting Functions

There are multiple identical or nearly identical implementations of shell quoting functions across different files in the actions package:

  1. testops/actions/system.go -> execShellQuote
  2. testops/actions/build.go -> shellQuote
  3. testops/actions/cleanup.go -> shellQuoteCleanup
  4. testops/actions/iscsi.go -> shellQuoteIscsi

Since all these files belong to the same actions package, they can share a single package-level function without any imports. Consolidating these into a single exported/package-level helper function (e.g., ShellQuote) improves maintainability and prevents copy-paste bugs.

Suggested change
func execShellQuote(s string) string {
func ShellQuote(s string) string {

Comment thread testops/actions/devops.go
Comment on lines +634 to +635
func extractVolumeField(info *blockapi.VolumeInfo, field string) (string, error) {
switch field {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Potential Nil Pointer Panic

extractVolumeField accesses fields of info (a pointer to blockapi.VolumeInfo) without checking if info is nil. If info is nil (e.g., due to an unhandled error or empty response from the API client), this function will panic.

Add a nil check at the beginning of extractVolumeField (and similarly in setISCSIVars on line 43) to prevent potential nil pointer panics.

func extractVolumeField(info *blockapi.VolumeInfo, field string) (string, error) {
	if info == nil {
		return "", fmt.Errorf("volume info is nil")
	}
	switch field {

Comment on lines +311 to +317
func gitSHAShort() string {
out, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Hanging Commands due to Missing Timeout/Context

gitSHAShort uses exec.Command without a context or timeout. If the git command hangs (e.g., due to a repository lock or file system issues), the entire test runner will hang indefinitely.

Use exec.CommandContext with a reasonable timeout (e.g., 5 seconds) to ensure the command terminates and does not block the execution pipeline indefinitely. Apply the same pattern to gitBranch (line 319) and hostname (line 327).

func gitSHAShort() string {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	out, err := exec.CommandContext(ctx, "git", "rev-parse", "--short", "HEAD").Output()
	if err != nil {
		return ""
	}
	return strings.TrimSpace(string(out))
}

pingqiu and others added 4 commits July 4, 2026 21:51
The unified gate build step never built seaweedfs-sw-rdma-kvcache, so the
kvcache loader smoke failed with UNIFIED_NO_KVCACHE. The lab filer (weed-uds)
also predates sw-rdma-kd's VFS conditional mutations, so the VFS cross-access
and read-matrix blocks hard-fail; skip them via SKIP_VFS=1 to gate the RDMA
object/loader hot path independently. Both were already live on the deployed
M01 copy; this lands them in source so a redeploy keeps them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant