test: nvmet/nvmet-tcp integration suite on a Talos/QEMU cluster - #439
Conversation
dc40909 to
e7ee4fe
Compare
A separate Go module under test/integration that boots a Talos cluster in QEMU and drives a real NVMe-oF fabric on it, so atlas's detection is exercised against a kernel rather than against a fixture. The pieces are a cluster harness (talosctl's QEMU provisioner, plus each node's QEMU monitor so a host can be faulted from outside it), an nvmet fabric built through configfs with an initiator that writes to /dev/nvme-fabrics the way atlas's own FabricsConnector does, and a control-plane simulator generated from shared/openapi.json so the compiler notices when the spec grows a path. The suite it carries forces the defect that costs the most to reach by hand: two targets on two nodes sharing one NQN, one exporting the namespace and one exporting nothing, which leaves a host with a single subsystem, two live controllers, and only one of them serving a path. Gated behind SB_INTEGRATION, so the cluster-booting tests skip themselves elsewhere. Ported from the integration-test-framework branch, whose history had no merge base with main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
shared/openapi.json is the contract atlas-lib's client, the integration suite's control-plane simulator, and the operator's spec-backed mock are generated or checked against. It is exported from the control plane, so it goes stale silently: nothing here fails when sbcli grows an endpoint, and the generated code keeps compiling against endpoints that no longer exist. The workflow exports it from sbcli, regenerates what the spec feeds, and opens a pull request when it moved, reusing one branch so a reviewer sees one proposal rather than a nightly pile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
15a7dcc to
f2be764
Compare
There was a problem hiding this comment.
🟡 Changes recommended
There are a couple of concrete workflow/house-style breakages in the new integration harness (sudo handling and terminology gating) that should be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds a new end-to-end NVMe-oF integration test harness under test/integration/ that boots a Talos Kubernetes cluster in QEMU, stands up a real nvmet/nvmet-tcp fabric inside it, and validates atlas-lib’s sysfs-based NVMe-oF inspection against kernel state (not fixtures). It also adds automation to keep the shared control-plane OpenAPI spec in sync.
Changes:
- Introduce a standalone Go module (
test/integration) with a Talos/QEMU cluster provisioner, NVMe-oF fabric driver, and integration suites (smoke + “controller not contributing” defect reproduction). - Add a generated control-plane simulator (
cpsim) based onshared/openapi.json, plus stubs for unimplemented endpoints and unit tests for the simulator and monitor protocol parsing. - Add CI workflows for running the integration suite and for nightly OpenAPI export + PR creation, plus a helper script to export/summarize spec changes.
File summaries
| File | Description |
|---|---|
| test/integration/suites/smoke_test.go | Smoke suite to validate kernel modules/configfs access and QEMU monitor faulting. |
| test/integration/suites/defect_test.go | Reproduces and asserts atlas-lib’s “controller not contributing” defect from captured sysfs state. |
| test/integration/Makefile | Local build/vet/lint/test entrypoints and preflight checks for required tooling/privileges. |
| test/integration/go.mod | New standalone integration-test module with a replace to the repo’s atlas-lib/. |
| test/integration/go.sum | Dependency lockfile for the integration module. |
| test/integration/.gitignore | Prevents accidental commit of Talos-generated configs containing sensitive material. |
| test/integration/fabric/target.go | Configures nvmet subsystems/ports via configfs and backs namespaces with loop devices. |
| test/integration/fabric/initiator.go | Connects/disconnects/rescans controllers via /dev/nvme-fabrics and sysfs triggers. |
| test/integration/fabric/nodeshell.go | Privileged per-node pod “shell” for running commands against node kernel/configfs. |
| test/integration/fabric/sysfs.go | Dumps node NVMe sysfs to a text snapshot and reconstructs it locally for resolvers. |
| test/integration/fabric/sysfs_test.go | Unit tests validating sysfs reconstruction and replay against an existing fixture. |
| test/integration/cluster/talos.go | Talos/QEMU cluster lifecycle: create/destroy, config patching, kubeconfig fetch, diagnostics. |
| test/integration/cluster/talos_test.go | Tests for cluster-name length/path constraints (Unix socket sun_path limits). |
| test/integration/cluster/monitor.go | QEMU HMP monitor client for fault injection (freeze/thaw/link down/power off). |
| test/integration/cluster/monitor_test.go | Unit tests for monitor protocol framing/echo-stripping/netdev discovery. |
| test/integration/cluster/kubectl.go | Thin kubectl-based Kubernetes access layer (apply/exec/waits). |
| test/integration/controlplane/connect.go | Builds /connect responses matching control-plane conventions, including secrets-in-commandline behavior. |
| test/integration/controlplane/state.go | In-memory state model for clusters/nodes/pools/volumes used by the simulator. |
| test/integration/controlplane/server.go | HTTP server wrapper with auth and DTO conversion helpers. |
| test/integration/controlplane/server_test.go | Drives simulator via atlas-lib’s client to ensure strict decode + behavior correctness. |
| test/integration/controlplane/handlers.go | Implemented control-plane endpoints (health/ready + node/pool/volume/connect reads). |
| test/integration/controlplane/cpsim.gen.go | Generated OpenAPI models/router/server interface used to enforce spec drift detection. |
| test/integration/controlplane/unimplemented.gen.go | Generated 501 stubs for every endpoint the simulator doesn’t implement. |
| test/integration/controlplane/gen.go | go:generate entrypoint wiring for codegen + stub generation. |
| test/integration/controlplane/tools.go | Tool pin for oapi-codegen via a tools build tag. |
| test/integration/controlplane/oapi-codegen.yaml | oapi-codegen config (models + std-http-server + overlay). |
| test/integration/controlplane/overlay.yaml | Spec overlay patching response/parameter gaps in the upstream OpenAPI export. |
| test/integration/controlplane/gen/main.go | Generator producing unimplemented.gen.go and validating handler/interface alignment. |
| shared/export-openapi.py | Exports the v2 OpenAPI spec from sbcli’s FastAPI app and summarizes structural diffs. |
| .github/workflows/repo_openapi_sync.yaml | Nightly job exporting shared/openapi.json, regenerating consumers, and opening/updating a PR. |
| .github/workflows/integration_test.yaml | CI workflow to boot Talos in QEMU (/dev/kvm required) and run the integration module tests. |
Review details
Files not reviewed (1)
- test/integration/controlplane/unimplemented.gen.go: Generated file
Suppressed comments (1)
test/integration/cluster/talos.go:183
- When
cfg.Sudois true,talosctlis executed undersudoviaCombinedOutput(). If sudo needs an interactive password prompt, it will block with the prompt hidden insidego testoutput capture, making the run look hung. It would be safer to fail fast with a clear error if sudo cannot run non-interactively (or credentials are not cached).
if *cfg.Sudo {
if _, err := exec.LookPath("sudo"); err != nil {
return nil, fmt.Errorf("sudo not found, and the QEMU provisioner needs root: %w", err)
}
}
- Files reviewed: 28/31 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The smoke test's monitor subtest deadlocked every run since it was added, and the job was killed at its own 45-minute cap with no output from the suite at all. QEMU's socket chardev serves one client at a time. The subtest held a monitor open and then called Freeze, which opens its own: the kernel accepted that second connection into the listen backlog and QEMU never serviced it, so no banner and no prompt ever arrived. Cluster.Monitor read the banner with no deadline on the connection, and Monitor.read observes the context only between reads, so a read that has already blocked cannot be reached by it. The test's thirty-minute context bounded nothing. setDeadline is extracted from Command and applied to the banner read too, which is the one exchange that had none, and the subtest opens a monitor per question instead of holding one across Freeze and Thaw. The regression test stands a socket up bound but unattended, the way the chardev behaves once it has a client, and asserts that Monitor gives up on it inside the caller's deadline. It cannot cover the subtest's own shape: the fake monitor serves every connection it accepts, which is why the collision was invisible off a cluster in the first place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seven consecutive runs of this workflow were killed at the job's 45-minute cap having printed nothing from the suite, and the three reasons were all in the workflow rather than in the test that hung. The suites now run as their own step. go test buffers a package's output while other packages are still running and prints it when the package finishes, so a package that never finishes prints nothing at all, and a boot failure and a deadlock read identically. Their timeout is 35 minutes, under the job's own, so that a wedge is a panic carrying every goroutine's stack instead of a cancellation carrying no output, with room left for the diagnostic steps. Diagnostics also collect on a canceled run. A job the runner kills for exceeding its timeout is canceled rather than failed, so the step that reads cluster state and serial logs was skipped on exactly the runs that needed it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An end-to-end integration suite that boots a Talos cluster in QEMU and drives a
real NVMe-oF fabric on it, so atlas's detection is exercised against a kernel
rather than against a fixture.
It lives in
test/integration/as its own Go module, so its Kubernetes and QEMUdependencies stay out of atlas-lib and out of the driver it exercises.
What is here
fabric/— the nvmet and nvmet-tcp core.target.gobuilds a subsystemand its TCP port through configfs; two targets sharing an NQN need disjoint
CntlIDMin/CntlIDMaxranges, which is what makes the host merge them ratherthan reject the second controller as a duplicate cntlid.
initiator.goconnects by writing an options line to
/dev/nvme-fabricsand drivingdelete_controller/rescan_controller, the same mechanism atlas's ownFabricsConnectoruses, so the fabric is established the way the code undertest establishes one — and no nvme-cli is needed in the pod.
cluster/— talosctl's QEMU provisioner, plus each node's QEMU humanmonitor, so a host can be faulted from outside it.
controlplane/— a control-plane simulator generated fromshared/openapi.json, so the compiler is what notices when the control planegrows a path or renames one. 9 of 98 endpoints are implemented; the rest answer
501. The generated files are committed and
make generate-checkfails on drift.suites/—TestSmoke, andTestDefect_ControllerNotContributing, whichforces the defect that costs the most to reach by hand: two targets on two
nodes sharing one NQN, one exporting the namespace and one exporting nothing,
leaving a host with a single subsystem, two live controllers, and only one of
them serving a path.
The cluster-booting tests are gated behind
SB_INTEGRATION, so they skipthemselves everywhere else.
make preflightchecks the tools and privileges arun needs and pre-warms sudo, since the QEMU provisioner needs root and the
prompt would otherwise land invisibly mid-run.
The second commit adds the nightly OpenAPI export that keeps
shared/openapi.jsonfrom going stale silently. It is separable from the suite;say the word and it can move to its own pull request.
History
Force-pushed onto a clean base. The previous head had no merge base with
main— its first commit was a whole-repo snapshot, which is why rebasing it conflicted
on essentially every file. The framework was re-applied onto current
mainastwo commits instead. No review comments existed on the old head.
Verification
go mod tidy, which bumped two indirect dependencies.go test ./...green for the four packages that do not need a cluster.make generate-checkpasses:shared/openapi.jsonis unchanged, so thecommitted simulator is current.
gofmtclean, and the house-style quality gate passes.Not yet verified: no Talos cluster has been booted since the rebase, so the
cluster-booting paths are unexercised on this base. That is what the
Integration: Testsworkflow is for.Known gaps
cluster/kubectl.go:15trips the terminology gate onKubectl, thegodoc-mandated leading identifier of the method it documents. Backticks would
reach pkg.go.dev as literal characters, which is the same reason the gate
already exempts a package clause. Resolving it properly means extending
EXEMPT_PHRASESincheck-terminology.py, which is shared tooling and leftout of this change.
repo_lint.yaml's matrix, which still covers threemodules.
integration_test.yamlrunsgo vetand the localMakefilehas alinttarget, but golangci-lint does not run in CI for it.