Skip to content

test: nvmet/nvmet-tcp integration suite on a Talos/QEMU cluster - #439

Merged
noctarius merged 7 commits into
mainfrom
integration-test-framework
Sep 4, 2026
Merged

noctarius merged 7 commits into
mainfrom
integration-test-framework

Conversation

@noctarius

@noctarius noctarius commented Aug 17, 2026 •

Copy link
Copy Markdown
Collaborator

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 QEMU
dependencies stay out of atlas-lib and out of the driver it exercises.

What is here

  • fabric/ — the nvmet and nvmet-tcp core. target.go builds a subsystem
    and its TCP port through configfs; two targets sharing an NQN need disjoint
    CntlIDMin/CntlIDMax ranges, which is what makes the host merge them rather
    than reject the second controller as a duplicate cntlid. initiator.go
    connects by writing an options line to /dev/nvme-fabrics and driving
    delete_controller/rescan_controller, the same mechanism atlas's own
    FabricsConnector uses, so the fabric is established the way the code under
    test establishes one — and no nvme-cli is needed in the pod.
  • cluster/ — talosctl's QEMU provisioner, plus each node's QEMU human
    monitor, so a host can be faulted from outside it.
  • controlplane/ — a control-plane simulator generated from
    shared/openapi.json, so the compiler is what notices when the control plane
    grows a path or renames one. 9 of 98 endpoints are implemented; the rest answer
    501. The generated files are committed and make generate-check fails on drift.
  • suites/ — TestSmoke, and TestDefect_ControllerNotContributing, which
    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,
    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 skip
themselves everywhere else. make preflight checks the tools and privileges a
run 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.json from 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 main as
two commits instead. No review comments existed on the old head.

Verification

  • Builds and vets clean against current atlas-lib; the only wiring change was
    go mod tidy, which bumped two indirect dependencies.
  • go test ./... green for the four packages that do not need a cluster.
  • make generate-check passes: shared/openapi.json is unchanged, so the
    committed simulator is current.
  • gofmt clean, 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: Tests workflow is for.

Known gaps

  • cluster/kubectl.go:15 trips the terminology gate on Kubectl, the
    godoc-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_PHRASES in check-terminology.py, which is shared tooling and left
    out of this change.
  • The new module is not in repo_lint.yaml's matrix, which still covers three
    modules. integration_test.yaml runs go vet and the local Makefile has a
    lint target, but golangci-lint does not run in CI for it.

@noctarius
noctarius force-pushed the integration-test-framework branch 3 times, most recently from dc40909 to e7ee4fe Compare August 18, 2026 19:30
@noctarius noctarius self-assigned this Aug 20, 2026
@noctarius noctarius added this to the 26.4 milestone Aug 20, 2026
noctarius and others added 2 commits September 4, 2026 09:24
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>
@noctarius
noctarius force-pushed the integration-test-framework branch from 15a7dcc to f2be764 Compare September 4, 2026 07:29
@noctarius noctarius changed the title Integration test framework: Talos/QEMU cluster + nvmet fabric test: nvmet/nvmet-tcp integration suite on a Talos/QEMU cluster Sep 4, 2026
@noctarius
noctarius marked this pull request as ready for review September 4, 2026 07:35
Copilot AI lite review requested due to automatic review settings September 4, 2026 07:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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 on shared/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.Sudo is true, talosctl is executed under sudo via CombinedOutput(). If sudo needs an interactive password prompt, it will block with the prompt hidden inside go test output 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.

Comment thread test/integration/Makefile Outdated
Comment thread test/integration/cluster/kubectl.go Outdated
Comment thread test/integration/cluster/talos.go
noctarius and others added 5 commits September 4, 2026 09:42
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>
@noctarius
noctarius merged commit 838521a into main Sep 4, 2026
5 checks passed
@noctarius
noctarius deleted the integration-test-framework branch September 4, 2026 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants