Skip to content

test(e2e): capture per-node network diagnostics in cozyreport - #3213

Open
IvanHunters wants to merge 2 commits into
mainfrom
test/cozyreport-per-node-network-diagnostics
Open

IvanHunters wants to merge 2 commits into
mainfrom
test/cozyreport-per-node-network-diagnostics

Conversation

@IvanHunters

@IvanHunters IvanHunters commented Jul 6, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

Extends hack/cozyreport.sh with a net-diag section that captures per-satellite-node network state from the DaemonSet pods already running on the node: iptables-save + conntrack -L (from kube-ovn-cni cni-server), ovs-ofctl dump-flows br-int (from ovs-ovn openvswitch), and hubble observe + cilium-dbg endpoint list (from cilium cilium-agent). Follows #3209 (LINSTOR ErrorReport bundles) as the second half of closing the "cozyreport is blind to the host-networking path" gap surfaced by the PR #3044 vlselect flake investigation.

Why

Recurrent baseline flake in the sandbox: vlselect-generic-* on one satellite fails every livenessProbe with dial tcp <podIP>:9471: connect: connection refused for 49 s, while an identical sibling replica on another satellite runs clean. Cozyreport at end-of-job carries CiliumEndpoint state: ready, the pod-side listener log confirming the HTTP server bound at 0.0.0.0:9471 within 5 ms of start, and the kube-ovn CNI ADD trace completing for the pod IP — all say the path SHOULD work. Attribution of the drop to a specific host-side cause (stale conntrack from a previous pod that shared the IP, a Cilium policy drop that only Hubble sees, or an OVS-flow race between the CNI ADD and the iptables NAT chain) is not possible from the artefact because none of the host-side networking state is captured. Talos-side kubelet logs also come out as one-line stubs (error constructing client: failed to determine endpoints — separate cozyreport bug), so kubelet-view evidence is unavailable too. This PR closes exactly that observability gap.

Scope

hack/cozyreport.sh only, +129 lines under a new net-diag module block. E2E-diagnostic-only, no product-side change, no chart change.

Verified

Every load-bearing claim cross-checked against a live production cluster and the chart templates in-tree:

  • Label selectors match the actual DaemonSets: app=kube-ovn-cni (ovncni-ds.yaml), app=ovs (ovsovn-ds.yaml, not accidentally matched by app=ovs-dpdk), app.kubernetes.io/name=cilium-agent (only the agent, not operator/envoy/pre-flight/etc.).
  • Container names (cni-server, openvswitch, cilium-agent) match the pod specs.
  • Tools confirmed present: iptables-save, conntrack, ip in cni-server; ovs-ofctl in openvswitch; hubble, cilium-dbg in cilium-agent.
  • Hubble socket path /var/run/cilium/hubble.sock matches hubble.socketPath in the cilium chart values.
  • Timeout choice timeout 30 matches the upper bound the sibling hack/e2e-capture-dataplane.sh uses for heavy captures.
  • _keep_if_useful pruning drops empty files AND single-line "not present / not found / OCI runtime / socket missing / error:" stubs, so an image without an optional tool leaves nothing (not a stub-as-diagnostic).
  • timeout(1) availability guarded at block entry: whole section skipped cleanly on runners without it.

Related

Release note

NONE

Summary by CodeRabbit

  • New Features
    • Added an optional per-node network diagnostics section to report collection to capture deeper host-side connectivity dataplane evidence when available.
    • Diagnostics are captured in a best-effort, time-bounded way so report generation continues even if captures fail.
    • Empty or placeholder diagnostic outputs are automatically pruned, preventing clutter and removing uninformative sections.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a baseline per-node network diagnostics capture to hack/cozyreport.sh. The capture collects host-side and CNI dataplane evidence through bounded, best-effort commands, prunes empty or DIAG-SKIP:-only files, and preserves genuine output and failure text.

Changes

Network Diagnostics Capture

Layer / File(s) Summary
Diagnostic pruning contract
hack/e2e-capture-dataplane.sh, hack/capture-dataplane.bats
Adds _diag_prunable behavior and tests for removing empty or single-line DIAG-SKIP: files while retaining real captures and command failure output.
Baseline dataplane capture
hack/e2e-capture-dataplane.sh
Adds --baseline mode, shared pod lookup, per-node iptables, conntrack, OVS, Cilium, and bounded drop-monitor capture, with per-file pruning and early exit.
Cozyreport integration
hack/cozyreport.sh
Invokes baseline capture into net-diag with a five-minute timeout and prevents capture failures from aborting report generation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: androndo

Sequence Diagram(s)

sequenceDiagram
  participant CozyReport as cozyreport.sh
  participant CaptureScript as e2e-capture-dataplane.sh
  participant NodePods as Per-node diagnostic pods
  participant NetDiag as net-diag output directory

  CozyReport->>CaptureScript: Run --baseline with net-diag directory
  CaptureScript->>NodePods: Collect host and CNI dataplane diagnostics
  NodePods-->>CaptureScript: Capture output or command failure text
  CaptureScript->>NetDiag: Prune empty or single-line DIAG-SKIP files
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding per-node network diagnostics to cozyreport in e2e testing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/cozyreport-per-node-network-diagnostics

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.

@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files area/testing Issues or PRs related to testing (e2e, bats, unit tests) labels Jul 6, 2026
@IvanHunters
IvanHunters marked this pull request as ready for review July 6, 2026 07:51
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the 'cozyreport.sh' script to bridge the observability gap regarding host-side networking paths. By capturing critical state from CNI and OVS components, it provides the necessary evidence to diagnose recurrent connectivity flakes in the sandbox environment, such as stale conntrack entries, policy drops, or OVS flow races.

Highlights

  • Network Diagnostics Module: Added a new 'net-diag' section to 'hack/cozyreport.sh' to capture host-side networking state from DaemonSet pods.
  • Diagnostic Data Sources: Captures 'iptables-save' and 'conntrack' from kube-ovn-cni, 'ovs-ofctl' from openvswitch, and 'hubble' logs and 'cilium-dbg' endpoints from cilium-agent.
  • Robustness and Pruning: Implemented a '_keep_if_useful' utility to automatically prune empty or error-stub files, ensuring only meaningful diagnostic data is collected.
  • Execution Safety: Wrapped all remote commands in 'timeout 30' to prevent hangs and added checks for required binaries to ensure script stability.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@dosubot dosubot Bot added the area/ci Issues or PRs related to CI workflows, GitHub Actions, automation label Jul 6, 2026

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces a new section in hack/cozyreport.sh to collect per-node network diagnostics (such as iptables, conntrack, OVS flows, and Hubble/Cilium endpoint states) to help debug transient network probe failures. The review feedback suggests several shell scripting improvements: handling potential syntax errors in line-count checks, using standard POSIX flags for grep portability, replacing subshells running basename with POSIX shell parameter expansions for efficiency, and simplifying the directory cleanup logic by directly executing rmdir.

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.

Comment thread hack/cozyreport.sh Outdated
Comment on lines +405 to +408
if [ "$(wc -l < "$_f" 2>/dev/null)" -le 1 ] && \
grep --extended-regexp --quiet "not (present|enabled|found)|socket missing|OCI runtime|executable file not found|command terminated|error:|Error from server" "$_f" 2>/dev/null; then
rm -f "$_f"
fi

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.

medium

There are two issues in this conditional block:

  1. If wc -l fails or returns an empty string, the expression [ "$(wc -l < "$_f" 2>/dev/null)" -le 1 ] will result in a shell syntax error (e.g., sh: 1: [: -le: argument expected in dash/sh). Assigning the line count to a variable with a default fallback (e.g., ${_lines:-0}) avoids this.
  2. The flags --extended-regexp and --quiet are GNU-specific extensions and may not be supported on non-GNU platforms (such as macOS or Alpine/busybox environments). Using the standard POSIX flags -E and -q is more portable and universally supported.
Suggested change
if [ "$(wc -l < "$_f" 2>/dev/null)" -le 1 ] && \
grep --extended-regexp --quiet "not (present|enabled|found)|socket missing|OCI runtime|executable file not found|command terminated|error:|Error from server" "$_f" 2>/dev/null; then
rm -f "$_f"
fi
_lines=$(wc -l < "$_f" 2>/dev/null)
if [ "${_lines:-0}" -le 1 ] && \\
grep -E -q "not (present|enabled|found)|socket missing|OCI runtime|executable file not found|command terminated|error:|Error from server" "$_f" 2>/dev/null; then
rm -f "$_f"
fi

Comment thread hack/cozyreport.sh Outdated

for pod in $(kubectl -n cozy-kubeovn get pods -l app=kube-ovn-cni -o name 2>/dev/null); do
node=$(kubectl -n cozy-kubeovn get "$pod" -o jsonpath='{.spec.nodeName}' 2>/dev/null)
[ -z "$node" ] && node=$(basename "$pod")

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.

low

Using the POSIX shell parameter expansion ${pod##*/} is more efficient than spawning a subshell to run basename.

Suggested change
[ -z "$node" ] && node=$(basename "$pod")
[ -z "$node" ] && node=${pod##*/}

Comment thread hack/cozyreport.sh Outdated

for pod in $(kubectl -n cozy-kubeovn get pods -l app=ovs -o name 2>/dev/null); do
node=$(kubectl -n cozy-kubeovn get "$pod" -o jsonpath='{.spec.nodeName}' 2>/dev/null)
[ -z "$node" ] && node=$(basename "$pod")

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.

low

Using the POSIX shell parameter expansion ${pod##*/} is more efficient than spawning a subshell to run basename.

Suggested change
[ -z "$node" ] && node=$(basename "$pod")
[ -z "$node" ] && node=${pod##*/}

Comment thread hack/cozyreport.sh Outdated

for pod in $(kubectl -n cozy-cilium get pods -l app.kubernetes.io/name=cilium-agent -o name 2>/dev/null); do
node=$(kubectl -n cozy-cilium get "$pod" -o jsonpath='{.spec.nodeName}' 2>/dev/null)
[ -z "$node" ] && node=$(basename "$pod")

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.

low

Using the POSIX shell parameter expansion ${pod##*/} is more efficient than spawning a subshell to run basename.

Suggested change
[ -z "$node" ] && node=$(basename "$pod")
[ -z "$node" ] && node=${pod##*/}

Comment thread hack/cozyreport.sh Outdated
Comment on lines +475 to +477
if [ -z "$(ls -A "$DIR" 2>/dev/null)" ]; then
rmdir "$DIR" 2>/dev/null || true
fi

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.

low

Since rmdir only succeeds if the directory is empty, you can directly run rmdir "$DIR" 2>/dev/null || true without checking if the directory is empty first. This is simpler, avoids spawning a subshell for ls -A, and is consistent with how it is done on line 329.

Suggested change
if [ -z "$(ls -A "$DIR" 2>/dev/null)" ]; then
rmdir "$DIR" 2>/dev/null || true
fi
rmdir "$DIR" 2>/dev/null || true

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
hack/cozyreport.sh (1)

417-432: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the repeated exec+prune pattern into a helper; also improves error-message accuracy.

The command -v BIN >/dev/null 2>&1 && CMD || echo "BIN not present" pattern is duplicated 5 times (here, and again at lines 441-443, 458-460, 466-468). Beyond duplication, the &&/|| chaining collapses two distinct failure modes into one message: if the binary exists but the command itself fails (permission error, transient runtime issue, socket read timeout), the file still gets the generic "not present" message, which is misleading for an artifact whose whole purpose is accurate root-causing.

♻️ Illustrative refactor
+# _capture <namespace> <pod> <container> <outfile> <inner-shell-snippet>
+# Shared bounded-exec + prune wrapper; keep the per-command "how to detect
+# absence vs. failure" logic in the snippet itself.
+_capture() {
+  timeout 30 kubectl -n "$1" exec "$2" --container="$3" -- sh -c "$5" \
+    > "$4" 2>&1 || true
+  _keep_if_useful "$4"
+}

For the hubble case specifically, distinguishing "not enabled/no socket" from "observe command failed" would look like:

🔧 Attribution fix example (line 459)
-    sh -c 'command -v hubble >/dev/null 2>&1 && [ -S /var/run/cilium/hubble.sock ] && hubble observe --last 2000 --output=compact 2>&1 || echo "hubble not enabled or socket missing in cilium-agent"' \
+    sh -c 'if ! command -v hubble >/dev/null 2>&1; then echo "hubble not present in cilium-agent";
+           elif [ ! -S /var/run/cilium/hubble.sock ]; then echo "hubble socket missing in cilium-agent";
+           else hubble observe --last 2000 --output=compact 2>&1 || echo "hubble observe failed in cilium-agent"; fi' \
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/cozyreport.sh` around lines 417 - 432, The repeated `kubectl exec` plus
`_keep_if_useful` pattern should be refactored into a helper in
`hack/cozyreport.sh` so the repeated `command -v BIN ... && CMD || echo ...`
logic is centralized. Update the helper to distinguish “binary not present” from
“command failed” instead of using a single fallback message, and make the caller
pass the command name and a context-specific failure message. Apply this to the
`iptables-save`, `conntrack`, and other repeated artifact collection blocks so
the generated files report accurate root-cause details.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hack/cozyreport.sh`:
- Around line 411-413: The kube-ovn report loops still call kubectl get without
a hang guard, so they can block the whole script on a slow API server. Update
the pod and node lookup logic in cozyreport.sh, including the kube-ovn CNI loop
that uses kubectl get pods and kubectl get ... -o jsonpath, to run those kubectl
get calls through timeout just like the kubectl exec captures. Keep the existing
fallback behavior for node names so the report continues even when a lookup
times out.

---

Nitpick comments:
In `@hack/cozyreport.sh`:
- Around line 417-432: The repeated `kubectl exec` plus `_keep_if_useful`
pattern should be refactored into a helper in `hack/cozyreport.sh` so the
repeated `command -v BIN ... && CMD || echo ...` logic is centralized. Update
the helper to distinguish “binary not present” from “command failed” instead of
using a single fallback message, and make the caller pass the command name and a
context-specific failure message. Apply this to the `iptables-save`,
`conntrack`, and other repeated artifact collection blocks so the generated
files report accurate root-cause details.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 65343432-54fb-4ac5-bf3b-b6fa40d583ce

📥 Commits

Reviewing files that changed from the base of the PR and between f101426 and 4a96635.

📒 Files selected for processing (1)
  • hack/cozyreport.sh

Comment thread hack/cozyreport.sh Outdated
Comment on lines +411 to +413
for pod in $(kubectl -n cozy-kubeovn get pods -l app=kube-ovn-cni -o name 2>/dev/null); do
node=$(kubectl -n cozy-kubeovn get "$pod" -o jsonpath='{.spec.nodeName}' 2>/dev/null)
[ -z "$node" ] && node=$(basename "$pod")

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant section of the script
file="hack/cozyreport.sh"
wc -l "$file"
sed -n '360,460p' "$file"

# Find any timeout usage around kubectl in this file
rg -n 'timeout|kubectl get|kubectl exec|cozyreport' "$file"

Repository: cozystack/cozystack

Length of output: 13319


Wrap the kubectl get calls in timeout too. The no-hang guarantee only covers the kubectl exec captures; kubectl get pods and the per-pod get ... -o jsonpath lookups in all three loops can still block on a slow or wedged API server and stall the whole report.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/cozyreport.sh` around lines 411 - 413, The kube-ovn report loops still
call kubectl get without a hang guard, so they can block the whole script on a
slow API server. Update the pod and node lookup logic in cozyreport.sh,
including the kube-ovn CNI loop that uses kubectl get pods and kubectl get ...
-o jsonpath, to run those kubectl get calls through timeout just like the
kubectl exec captures. Keep the existing fallback behavior for node names so the
report continues even when a lookup times out.

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.

NOT LGTM — the plumbing is correct, but the section does not deliver two of the three diagnostics it promises, deletes genuine capture-failure evidence, and forks a divergent copy of a sibling capture whose output already lands in the same tarball.

Business context: cozyreport is blind to host-side networking state, so a recurrent probe-refused transient (pod bound and Ready, kubelet still gets connection refused) cannot be attributed after the fact from the uploaded artefact.

Credit first — every selector and container name is right, and I verified each against the shipped charts: app=kube-ovn-cni/cni-server (ovncni-ds.yaml:16,90), app=ovs/openvswitch (ovsovn-ds.yaml:22,85, correctly not matching app=ovs-dpdk), app.kubernetes.io/name=cilium-agent/cilium-agent (the label is on the pod template, daemonset.yaml:66,92). timeout 30 does match the sibling's heaviest capture (e2e-capture-dataplane.sh:266). What blocks is below the plumbing.

Blockers

B1: The Hubble capture can never produce output on the shipped platform

File: hack/cozyreport.sh:458-461
Issue: packages/system/cilium/values.yaml:13-14 sets hubble.enabled: false, overriding the upstream chart default of true (charts/cilium/values.yaml:1393). It is the only hubble key in the cozystack cilium config — nothing re-enables it.
Evidence: cilium-configmap.yaml:1036-1039 emits hubble-socket-path only under {{- if .Values.hubble.enabled }}, so with the flag off cilium-agent never creates /var/run/cilium/hubble.sock. The [ -S /var/run/cilium/hubble.sock ] guard therefore always fails, the echo "hubble not enabled or socket missing…" stub is written, and _keep_if_useful deletes it (single line, matches not (present|enabled|found)). Net result: two extra kubectl exec per node per report, zero bytes captured, on every cluster.
Impact: the block comment at :361-362 names hypothesis (b) — "blocked by a hostFirewall or Cilium policy drop that only Hubble sees" — as one of three the section exists to disambiguate. hostFirewall.enabled: true (values.yaml:11-12), so policy drops are a live hypothesis, and this ships the one instrument that cannot observe them. The comment at :367-369 also asserts every source has "the required binary preinstalled", which is not true for the hubble path.
Fix: capture drops with cilium-dbg monitor --type drop (needs no Hubble; the sibling already runs it, e2e-capture-dataplane.sh:306-311) and/or cilium-dbg bpf ct list global (sibling :414-416). Drop the hubble capture and the (b) claim, or enable Hubble.

B2: _keep_if_useful deletes real capture-failure evidence

File: hack/cozyreport.sh:399-409
Issue: the regex at :406 includes error: and Error from server. Since every capture redirects 2>&1, a failed kubectl exec writes its one-line error into the file — and the file is then silently removed.
Evidence: I ran the helper verbatim against real kubectl failure text. Error from server (BadRequest): pod … does not have a host assigned → deleted. error: unable to upgrade connection: container not found ("cilium-agent") → deleted. A legitimate header-only capture is correctly kept, so the deletions are specifically the exec failures.
Impact: a missing file now means "node healthy", "binary absent", or "the capture broke" — three different conclusions the artefact can no longer distinguish. An agent that is CrashLooping or Terminating (precisely the interesting case) leaves no trace at all. docs/agents/e2e-testing.md §8 is explicit that failure must be self-explanatory and diagnostics are first-class. The pruning is also inconsistent: a one-line OCI runtime error is deleted, but a multi-line exec error is kept and reads as a real capture.
Fix: keep the stubs, or fold them into one net-diag/capture-errors.txt index. Do not delete a record of a failed capture.

B3: conntrack drops the /proc/net/nf_conntrack fallback the sibling deliberately carries

File: hack/cozyreport.sh:428-430
Issue: command -v conntrack … || echo "conntrack not present" with no fallback.
Evidence: e2e-capture-dataplane.sh:429-433 does the same capture and explicitly comments "Prefer the conntrack CLI; fall back to /proc/net/nf_conntrack when it is absent from the image" — in-repo evidence that the cni-server image may not ship the CLI. If it doesn't, this emits a one-line "not present" stub, which B2 then deletes.
Impact: conntrack is hypothesis (a) in the block's own comment (:360-361, stale conntrack from a prior pod on the same IP) — the headline one. In the absent-CLI case the report carries zero conntrack evidence and zero indication of why.
Fix: port the /proc/net/nf_conntrack fallback.

B4: No aggregate timeout bound — the worst case can cost the entire artefact

File: hack/cozyreport.sh:388
Issue: each exec is bounded at 30 s, but the section has none in aggregate: 5 execs × N nodes. On a 3-node cluster with a wedged API server that is ~7.5 minutes appended to report collection, and it scales linearly with node count.
Evidence: the repo already recognised this hazard for this exact class of capture — hack/cozytest.sh:123 wraps the sibling in timeout -k 30 600. The new block gets no equivalent, and neither does its call site: packages/core/testing/Makefile:45 invokes cozyreport.sh bare, and the CI step (.github/workflows/pull-requests.yaml:674-678) is if: always() with no timeout-minutes, inside a job capped at 180 minutes (:495).
Impact: on a long full-suite run already near the cap, the added exec time can push the job past the timeout. The job is cancelled, Upload cozyreport.tgz (:680-685) never runs, and the whole diagnostic artefact is lost — in exactly the failure case this change is built for.
Fix: wrap the section in an outer timeout -k.

B5: No tests, in an area that has them

File: hack/cozyreport.sh:399-409
Issue: _keep_if_useful is a pure, self-contained, file-deleting helper whose semantics are a 7-alternative regex plus an arbitrary ≤ 1 line threshold. A regression in that regex silently eats real captures — and per B2 it already does.
Evidence: hack/capture-dataplane.bats exists for precisely this purpose, carries 15 @test cases pinning the sibling's pure helpers against mock input via an E2E_CAPTURE_DATAPLANE_LIB sourcing guard (:40-42), and states in its own header that only the pure logic is unit-testable. There are 18 hack/*.bats files following that pattern.
Fix: minimum coverage — empty file → deleted; one-line Error from server stub → kept (per B2); real multi-line capture → kept; a legitimate one-line capture containing the word error: → kept. The sourcing-guard pattern is already established.

B6: Four of the five captures already exist in the sibling, whose output lands in the same tarball

File: hack/cozyreport.sh:411-470
Issue: cilium-dbg endpoint list (:466 vs sibling :301), ovs-ofctl dump-flows br-int (:441 vs :324), hubble observe (:458 vs :314), conntrack -L (:428 vs :432, with the fallback). Only iptables-save is genuinely new.
Evidence: hack/cozytest.sh:123 writes the sibling's output to $_snap/dataplane, and hack/cozyreport.sh:485-489 copies $SNAP_DIR into $REPORT_DIR/snapshots. So on a failing run the tarball carries both snapshots/<test>/dataplane/node-<node>.txt and net-diag/<node>-*.txt with overlapping content captured at different times — double the exec load, and confusing to read.
Impact: the intent is legitimate and worth keeping — the sibling only fires on a failed test with NotReady pods, so a baseline per-node capture on every report genuinely covers a self-healing transient the sibling misses. But this forks a divergent copy that already disagrees with the original on day one: no /proc fallback, no monitor --type drop, a different cilium selector, and namespaces hardcoded here (cozy-kubeovn/cozy-cilium) versus env-overridable there (COZY_KUBEOVN_NS/COZY_CILIUM_NS, :211-212). Two copies of the same CNI topology knowledge now have to be kept in sync.
Fix: factor the per-node capture into one helper both callers share, or give e2e-capture-dataplane.sh an all-nodes/baseline mode that cozyreport.sh invokes — it already has capture_node() (:393) and pod_on_node() (:274). Add iptables-save there. Same value, far less surface, and it inherits the existing tests.

Non-blocking follow-ups

  1. hack/cozyreport.sh:385-387 — the command -v timeout guard is dead code. The only environment that runs this script is the e2e sandbox, and packages/core/testing/images/e2e-sandbox/Dockerfile:1 is FROM ubuntu:24.04, so coreutils timeout is always present. It is also inconsistent with e2e-capture-dataplane.sh and cozytest.sh:123, which both use timeout unguarded. Harmless today, but if it ever fired it would silently skip the entire section on one line of output.
  2. hack/cozyreport.sh:447 — the cilium selector diverges from the sibling (app.kubernetes.io/name=cilium-agent here, k8s-app=cilium at e2e-capture-dataplane.sh:292). Both are correct; two scripts in the same pipeline should agree.
  3. hack/cozyreport.sh:388-477 — the else body is not indented, which makes the closing fi at :478 hard to pair. An early-skip guard would read better.
  4. There is no shellcheck gate in CI, so none of the above is caught mechanically — worth considering separately.

On the existing bot comments: the wc -l "syntax error" concern does not hold — the [ ! -s "$_f" ] early return guarantees the file exists and is non-empty before wc runs. The long-flag portability concern likewise does not apply (Ubuntu sandbox, GNU grep; the script already uses free -m, ps auxww, base64 -d). Wrapping the kubectl get calls in timeout is not the right scope either: kubectl get only blocks when the API server is unreachable, at which point this script has already hung ~60 unguarded calls earlier — the calls that can hang against a healthy API server are the execs, and those are bounded. B4 is the version of that concern that actually bites.

Comment thread hack/cozyreport.sh Outdated
# even when the CLI does. Sized so a probe-drop cascade over a
# 5-minute window still fits, and a healthy node's log stays small.
timeout 30 kubectl -n cozy-cilium exec "$pod" --container=cilium-agent -- \
sh -c 'command -v hubble >/dev/null 2>&1 && [ -S /var/run/cilium/hubble.sock ] && hubble observe --last 2000 --output=compact 2>&1 || echo "hubble not enabled or socket missing in cilium-agent"' \

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.

This capture can never produce output on the shipped platform. packages/system/cilium/values.yaml:13-14 sets hubble.enabled: false (upstream default is true), and cilium-configmap.yaml:1036-1039 emits hubble-socket-path only under {{- if .Values.hubble.enabled }} — so cilium-agent never creates /var/run/cilium/hubble.sock. The [ -S ... ] guard always fails, the stub is written, and _keep_if_useful deletes it. Two execs per node per report, zero bytes, forever.

This also leaves hypothesis (b) from the block comment ("a Cilium policy drop that only Hubble sees") unaddressed — while hostFirewall.enabled: true, so it is a live hypothesis. cilium-dbg monitor --type drop needs no Hubble and would actually answer it; the sibling already runs it at e2e-capture-dataplane.sh:306-311.

Comment thread hack/cozyreport.sh Outdated
return
fi
if [ "$(wc -l < "$_f" 2>/dev/null)" -le 1 ] && \
grep --extended-regexp --quiet "not (present|enabled|found)|socket missing|OCI runtime|executable file not found|command terminated|error:|Error from server" "$_f" 2>/dev/null; then

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.

error: and Error from server in this regex delete genuine capture failures. Every capture redirects 2>&1, so a failed kubectl exec writes its one-line error here and the file is then removed.

I ran the helper verbatim against real kubectl failure text:

  • Error from server (BadRequest): pod ... does not have a host assigned → deleted
  • error: unable to upgrade connection: container not found ("cilium-agent") → deleted

A missing file now means "node healthy", "binary absent", or "the capture broke" — three different conclusions the artefact can no longer tell apart. An agent that is CrashLooping (the interesting case) leaves no trace. docs/agents/e2e-testing.md §8: failure must be self-explanatory, diagnostics are first-class.

The pruning is also inconsistent: a one-line OCI runtime error is deleted, but a multi-line exec error is kept and reads as a real capture. Keep the stubs, or fold them into a single net-diag/capture-errors.txt index.

Comment thread hack/cozyreport.sh Outdated
# recent entries — an operator chasing a specific 5-tuple should grep
# for the podIP/port rather than assume the tail is missing.
timeout 30 kubectl -n cozy-kubeovn exec "$pod" --container=cni-server -- \
sh -c 'command -v conntrack >/dev/null 2>&1 && conntrack -L 2>/dev/null | head -n 5000 || echo "conntrack not present in cni-server"' \

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.

No /proc/net/nf_conntrack fallback. The sibling does the same capture and carries one deliberately (e2e-capture-dataplane.sh:429-433), with the comment "Prefer the conntrack CLI; fall back to /proc/net/nf_conntrack when it is absent from the image" — in-repo evidence that cni-server may not ship the CLI.

If it doesn't, this emits a one-line "not present" stub, which _keep_if_useful then deletes. Conntrack is hypothesis (a) in this block's own comment — the headline one — so the report would carry zero conntrack evidence and zero indication of why. Please port the fallback.

Comment thread hack/cozyreport.sh
if ! command -v timeout >/dev/null 2>&1; then
echo " timeout(1) not present -- skipping per-node network diagnostics"
else
echo "Collecting per-node network diagnostics..."

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.

Each exec is bounded at 30 s, but the section has no aggregate bound: 5 execs × N nodes. On a 3-node cluster with a wedged API server that is ~7.5 min appended to report collection, scaling linearly with node count.

The repo already mitigates this for the same class of capture — hack/cozytest.sh:123 wraps the sibling in timeout -k 30 600. Here there is no equivalent, and no bound at the call site either: packages/core/testing/Makefile:45 invokes the script bare, and the CI step is if: always() with no timeout-minutes inside a job capped at 180 min.

Worst case, the added time pushes the job past its cap, the job is cancelled, Upload cozyreport.tgz never runs, and the entire artefact is lost — in exactly the failure case this change exists for. Please wrap the section in an outer timeout -k.

Comment thread hack/cozyreport.sh Outdated
fi
}

for pod in $(kubectl -n cozy-kubeovn get pods -l app=kube-ovn-cni -o name 2>/dev/null); do

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.

Four of the five captures here already exist in hack/e2e-capture-dataplane.sh: cilium-dbg endpoint list (:301), ovs-ofctl dump-flows br-int (:324), hubble observe (:314), conntrack -L (:432, with the /proc fallback). Only iptables-save is new.

And its output already lands in this same tarball: cozytest.sh:123 writes it to $_snap/dataplane, and cozyreport.sh:485-489 copies $SNAP_DIR into the report. So a failing run ships both trees with overlapping content captured at different times.

The intent is right — the sibling only fires on a failed test with NotReady pods, so a baseline per-node capture on every report does cover the self-healing transient it misses. But this is a divergent copy that already disagrees on day one: no /proc fallback, no monitor --type drop, a different cilium selector, and namespaces hardcoded here versus env-overridable there (COZY_KUBEOVN_NS/COZY_CILIUM_NS, :211-212).

Suggest giving e2e-capture-dataplane.sh an all-nodes/baseline mode and calling it from here — it already has capture_node() (:393) and pod_on_node() (:274). Add iptables-save there. Same value, one copy of the topology knowledge, and it inherits the existing bats coverage.

Comment thread hack/cozyreport.sh Outdated
# `timeout(1)` from coreutils is a hard dependency of this block. Skip
# the whole section on runners without it rather than filling the tree
# with `sh: 1: timeout: not found` stubs.
if ! command -v timeout >/dev/null 2>&1; then

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.

Non-blocking: this guard is dead code. The only environment that runs cozyreport.sh is the e2e sandbox, and packages/core/testing/images/e2e-sandbox/Dockerfile:1 is FROM ubuntu:24.04 — coreutils timeout is always present. It is also inconsistent with e2e-capture-dataplane.sh and cozytest.sh:123, which use timeout unguarded.

Harmless today, but if it ever did fire it would silently skip the entire section behind a single line of output.

@myasnikovdaniil myasnikovdaniil 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.

LGTM.

Diagnostic-only e2e tooling — a single file, no product or chart change. Every load-bearing selector / namespace / container / socket path checks out against the in-tree charts: cozy-kubeovn + app=kube-ovn-cni, app=ovs (correctly distinct from app=ovs-dpdk), cozy-cilium + app.kubernetes.io/name=cilium-agent, and the hubble socket /var/run/cilium/hubble.sock. Fail-safe throughout — every exec is timeout 30-bounded and || true, the loops self-guard on empty pod lists, the empty parent dir is cleaned up, and the whole block is skipped when timeout(1) is absent. One cosmetic nit inline.

Comment thread hack/cozyreport.sh Outdated
return
fi
if [ "$(wc -l < "$_f" 2>/dev/null)" -le 1 ] && \
grep --extended-regexp --quiet "not (present|enabled|found)|socket missing|OCI runtime|executable file not found|command terminated|error:|Error from server" "$_f" 2>/dev/null; then

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.

Consistency nit: grep --extended-regexp --quiet are GNU long options in a #!/bin/sh script — the short forms -E -q read the same and match the surrounding one-liners. Fails safe either way (a busybox grep would exit 2 and just keep an un-pruned stub, never lose data).

@myasnikovdaniil

Copy link
Copy Markdown
Contributor

IvanHunters CI is green, but this PR remains blocked by the requested-changes review. I rechecked current head 4a9663570; the blockers are still present:

  • Hubble is disabled in the shipped Cilium values, so the Hubble capture cannot produce data.
  • _keep_if_useful deletes one-line kubectl exec failures, hiding broken captures.
  • conntrack capture lacks the existing /proc/net/nf_conntrack fallback.
  • per-command timeouts have no aggregate bound across commands and nodes.
  • four collectors diverge from overlapping collectors in e2e-capture-dataplane.sh.
  • the new pruning and collection behavior has no regression tests.

Please address these and rebase after #3421 lands because it changes the same cozyreport.sh area. Ping me after updating; I’ll rerun the focused tests and re-review promptly.

Follows #3209 (LINSTOR ErrorReports) with the second half of the
"cozyreport is blind to the host-networking path" gap surfaced by
the PR #3044 vlselect flake investigation (run 28772259934):
`vlselect-generic-*` on one satellite hit `dial tcp <podIP>:9471:
connect: connection refused` on every livenessProbe for 49s and got
CrashLoopBackOff, while the sibling replica on another satellite —
same pod spec, same probe config, same image — ran clean. The
cozyreport artefact carried the CiliumEndpoint `state: ready` line,
the pod-side listener log confirming the HTTP server was bound at
0.0.0.0:9471 within 5 ms of start, and the kube-ovn CNI ADD trace
completing for the pod IP well before the first probe — every one
of which says the path SHOULD have worked. Attribution of the drop
to a specific host-side cause (stale conntrack from a previous pod
that shared the IP, a Cilium policy drop that only Hubble sees, or
an OVS-flow race between the CNI ADD and the iptables NAT chain)
was not possible from the artefact because none of the host-side
networking state is captured. Talos-side kubelet logs come out as
one-line stubs (`error constructing client: failed to determine
endpoints` — a separate cozyreport bug), so kubelet-view evidence
is unavailable too.

Add a `net-diag` section that runs after the sandbox-host block and
copies four artefacts per satellite node:

- `<node>-iptables.txt` — `iptables-save` from `kube-ovn-cni` (all
  tables in one blob so grepping across chains does not need a
  per-table unpack step).
- `<node>-conntrack.txt` — `conntrack -L` first 5000 lines, from the
  same pod; the head cap keeps the file small on nodes with heavy
  tenant traffic and preserves the failure-window entries on the
  first-in-first-out flush semantics of the kernel table.
- `<node>-hubble-flows.txt` — `hubble observe --last 2000 --output=compact`
  from `cilium-agent`. Hubble flow log carries the verdict
  (FORWARDED / DROPPED / ERROR) with reason field per flow, which is
  the only source that tells apart a Cilium policy drop from a
  datapath race.
- `<node>-cilium-endpoints.txt` — `cilium-dbg endpoint list` for the
  per-endpoint policy state (identity, enforcement, controllers,
  status) that resolves "was the drop a policy decision" once hubble
  flags one.

Every collection call uses a DaemonSet pod that already exists on the
node with hostNetwork and the required binary preinstalled:
`iptables-save` + `conntrack` live in `kube-ovn-cni`'s cni-server
container; `hubble observe` + `cilium-dbg` live in the `cilium-agent`
container. Both label selectors were confirmed against a live
production cluster; both tool paths were confirmed present in the
current piraeus / cilium image tags. Every capture is guarded with
`|| true` and stderr redirection so a broken pod / missing tool /
transient exec failure never fails the surrounding cozyreport run,
and the parent directory is pruned when neither CNI is present on
the cluster (e.g. a cozyreport captured against a non-cozystack
cluster).

Shipped as diagnostic-only test infra; no product-side change, no
schema change, no chart change.

Signed-off-by: Ivan Okhotnikov <[email protected]>
Address the review on the per-node network diagnostics. Instead of a
divergent second copy of the CNI capture logic in cozyreport.sh, add a
--baseline mode to hack/e2e-capture-dataplane.sh that cozyreport.sh
invokes, so the namespaces, DaemonSet selectors, container names and the
conntrack CLI->/proc fallback have a single source of truth shared with
the failure-triggered capture.

- replace the hubble capture (hubble.enabled is false on cozystack, so
  the socket is never created and it can never produce output) with
  cilium-dbg monitor --type drop, which needs no Hubble;
- carry the /proc/net/nf_conntrack fallback for images without the
  conntrack CLI;
- keep records of failed captures: prune only empty files and this
  script's own DIAG-SKIP absence markers, never a real kubectl exec
  error, so a broken capture stays distinguishable from an absent tool;
- bound the whole capture with an aggregate timeout -k at the call site
  so a wedged API server cannot cost the entire cozyreport artefact;
- unit-test the pruning predicate in hack/capture-dataplane.bats.

Signed-off-by: IvanHunters <[email protected]>
@IvanHunters
IvanHunters force-pushed the test/cozyreport-per-node-network-diagnostics branch from 4a96635 to 1f6bc1b Compare July 22, 2026 18:40

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
hack/e2e-capture-dataplane.sh (1)

300-349: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Baseline captures duplicate existing capture_node logic in the same file.

ovs-ofctl dump-flows br-int (325-331) and the cilium-dbg endpoint list / bounded cilium-dbg monitor --type drop pair (333-349) in capture_baseline re-implement near-identical logic already present in capture_node (unchanged, further down this file). Now that both live in this one script, factor the shared exec+redirect patterns into small helpers (e.g. _capture_ovs_flows, _capture_cilium_endpoints, _capture_cilium_drops) parameterized on destination (file vs. appended section), so the two collection modes can't drift again the way the old cozyreport.sh copy did.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/e2e-capture-dataplane.sh` around lines 300 - 349, Factor the duplicated
OVS and Cilium capture commands from capture_baseline and capture_node into
shared helpers such as _capture_ovs_flows, _capture_cilium_endpoints, and
_capture_cilium_drops. Parameterize each helper for its destination or append
mode, then replace both collection paths with these helpers while preserving
their existing timeout, container, command, and output behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@hack/e2e-capture-dataplane.sh`:
- Around line 300-349: Factor the duplicated OVS and Cilium capture commands
from capture_baseline and capture_node into shared helpers such as
_capture_ovs_flows, _capture_cilium_endpoints, and _capture_cilium_drops.
Parameterize each helper for its destination or append mode, then replace both
collection paths with these helpers while preserving their existing timeout,
container, command, and output behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1d154cc3-307e-49c0-91a3-971deb42ef47

📥 Commits

Reviewing files that changed from the base of the PR and between 4a96635 and 1f6bc1b.

📒 Files selected for processing (3)
  • hack/capture-dataplane.bats
  • hack/cozyreport.sh
  • hack/e2e-capture-dataplane.sh

@myasnikovdaniil myasnikovdaniil 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.

Thanks—the shared --baseline implementation resolves the previous Hubble, conntrack-fallback, aggregate-timeout, duplication, and testability blockers. Three correctness issues remain inline. This head is also CONFLICTING/DIRTY against current main after #3421; please rebase and reconcile the overlapping diagnostics changes.

Validation: focused Bats passed, sh -n passed, and git diff --check passed. Full pre-commit run --all-files was run; it fails only because this stale head still uses .git/pre-commit.lock, while .git is a file in a linked worktree. #3421 fixed that on main, so please rerun pre-commit after rebasing. The current E2E failure is in the mariadb suite and appears unrelated, but CI should be rerun after the update.

_diag_prunable() {
_dp_f="$1"
[ -s "$_dp_f" ] || return 0
_dp_lines=$(wc -l < "$_dp_f" 2>/dev/null | tr -d ' ')

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.

wc -l counts newline bytes rather than logical records. A file containing DIAG-SKIP: header\nreal capture line with no trailing newline reports one line, so this function reads only the marker and prunes real evidence; I reproduced that result locally. Please count records (for example with awk 'END { print NR }') and add a regression test for a multi-line capture whose final record has no newline.

# iptables-save: every table (filter/nat/mangle/raw) in one blob -- the
# only capture genuinely new vs the failed-pod path.
timeout 25 kubectl exec -n "$KUBEOVN_NS" "$_cni" -c cni-server -- \
sh -c 'command -v iptables-save >/dev/null 2>&1 && iptables-save || echo "DIAG-SKIP: iptables-save not present in cni-server"' \

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.

This && ... || ... treats every non-zero iptables-save result as “not present.” A present binary that fails silently produces only the marker, which _diag_prune then deletes; a failure with stderr also gets a misleading absence marker. Please separate the presence check from execution so command failures remain in the artifact.

# /proc/net/nf_conntrack when the image lacks it (the same fallback the
# failed-pod path carries), and DIAG-SKIP only when BOTH are absent.
timeout 25 kubectl exec -n "$KUBEOVN_NS" "$_cni" -c cni-server -- \
sh -c 'if command -v conntrack >/dev/null 2>&1; then conntrack -L 2>/dev/null | head -n 5000; elif [ -r /proc/net/nf_conntrack ]; then head -n 5000 /proc/net/nf_conntrack; else echo "DIAG-SKIP: no conntrack CLI and no /proc/net/nf_conntrack in cni-server"; fi' \

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.

conntrack -L 2>/dev/null | head ... hides capture failures: stderr is discarded and the pipeline status comes from head (I reproduced conntrack returning 42 while the pipeline returned 0), leaving an empty file that is pruned. Please preserve the execution error and/or fall back to /proc when invocation fails, not only when the binary is absent, and cover that path with a regression test.

@github-actions

Copy link
Copy Markdown

This PR has had no activity for 60 days and was marked lifecycle/stale.
It will be closed in 14 days unless commented or labelled lifecycle/frozen.

@github-actions github-actions Bot added the lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale label Sep 22, 2026

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.

IvanHunters NOT LGTM. The rework fixes what I asked for, but the branch conflicts with main, a capture that times out still disappears without a trace, and the commit history and PR body describe code that is no longer there.

Where the earlier blockers stand (4693353604, on 4a9663570)

  • B1 (Hubble): closed. The hubble capture is gone and cilium-dbg monitor --type drop replaced it.
  • B2 (pruning deletes failures): half closed. A kubectl error line is now kept, since _diag_prunable only drops empty files and lone DIAG-SKIP: markers. An exec that produced nothing is still deleted, though. That is B2 below.
  • B3 (conntrack fallback): closed. The /proc/net/nf_conntrack fallback is in.
  • B4 (aggregate bound): closed. timeout -k 30 300 wraps the call at hack/cozyreport.sh:370. On the head's E2E run the baseline took 28s (20:48:10Z to 20:48:38Z).
  • B5 (tests): closed. hack/capture-dataplane.bats gains 8 cases for _diag_prunable, both kept and dropped paths. All 23 pass locally.
  • B6 (duplicate copy): closed in structure, since cozyreport now calls e2e-capture-dataplane.sh --baseline. The rebase decides whether that stays true (B1 below).

Is it still needed

Partly. Since July main has added a lot to the failure path: hack/e2e-chainsaw/.chainsaw.yaml now runs e2e-capture-dataplane.sh from the global error.catch, and that collector now reads a same-window healthy pod on each affected node as a reference (route and conntrack), next to the node-wide cilium-dbg endpoint list, monitor --type drop and ovs-ofctl dump-flows br-int. What main still lacks and this PR adds: iptables-save (no script in hack/ runs it), a whole-table conntrack dump (main only greps for one IP), and a capture on runs, or nodes, where no pod is NotReady. Those three are worth keeping. The drop monitor is weaker in baseline mode, see the follow-ups.

Blockers

B1: Rebase onto main

The branch conflicts with current main (b113cc77d) in hack/e2e-capture-dataplane.sh and hack/capture-dataplane.bats. hack/cozyreport.sh merges cleanly. This is more than a textual conflict. On main, pod_on_node is memoised and bounded, and the collector's reads go through dp_read_outcome and write the reason it came back empty to capture-notes.txt. The PR moves the July version of pod_on_node up the file. After the rebase capture_baseline has to use main's helpers, or it becomes the second copy of the topology knowledge that B6 asked to remove.

B2: A capture that times out or fails quietly is deleted

File: hack/e2e-capture-dataplane.sh:301-354

The capture leaves no record when an exec produces nothing. Each capture is timeout 25 kubectl exec ... > file 2>&1 || true followed by _diag_prune file. When timeout kills the exec before it writes anything, the file is empty and gets removed. I sourced the script and ran timeout 1 sh -c "sleep 5" > f; _diag_prunable f, which returns rc=124 and then PRUNED. The conntrack capture also runs conntrack -L 2>/dev/null | head, so a CLI that fails leaves an empty file too. kubectl get nodes (:301) has no ceiling of its own, and a failure there looks the same as a cluster with no nodes: the loop does nothing, rmdir removes net-diag/, and the report looks exactly like one from a cluster without kube-ovn or cilium. The same goes for the outer timeout -k 30 300 firing: nothing records it.

This is the silent loss that docs/agents/e2e-testing.md §8 rules out for this collector: "why a read produced nothing goes to capture-notes.txt beside the capture rather than only to the job log, because an empty result and a read that never answered are the same bytes on disk". Fix: record the exec's exit status (a 124 in particular) and every read that came back empty in a net-diag/capture-notes.txt before pruning, and only prune a file when a read that answered produced nothing.

B3: The docs name two callers

docs/agents/e2e-testing.md §8 says "Its two callers bound it differently, 600s from cozytest.sh and 300s from the Chainsaw catch", and the script header on main says it captures state "for NotReady pods". With this PR there is a third caller, cozyreport at 300s, and a mode that walks every node. Please update both in the same change.

B4: The commit history describes the wrong code

The repository merges with merge commits, so both commit bodies land in main.

  • 8fb1e11f describes <node>-hubble-flows.txt, hubble observe --last 2000, and pruning that the second commit removes. It also says the conntrack head cap "preserves the failure-window entries on the first-in-first-out flush semantics of the kernel table". conntrack -L dumps in hash-table order, not by age, so head -n 5000 keeps an arbitrary 5000 entries.
  • 1f6bc1be opens with "Address the review on the per-node network diagnostics". That is review-cycle wording, not a reason.
  • hack/capture-dataplane.bats:239 puts "(PR review blocker B2)" into a code comment.

Please squash into one commit whose message describes the final design, and drop the review reference from the bats comment.

B5: The PR body describes the first version

The body becomes the merge commit message. It still says "hack/cozyreport.sh only, +129 lines under a new net-diag module block", lists hubble observe, says _keep_if_useful drops "error:" stubs, and says a timeout(1) guard skips the section. None of that is in the head. Please rewrite it for the --baseline design.

Non-blocking follow-ups

  1. I suggested cilium-dbg monitor --type drop in the last round, but in baseline mode it only watches the 8 seconds when the report runs, which is usually long after any transient. So it cannot answer "was the refused probe a policy drop". The failure-path collector already runs it at the moment of failure. Dropping it here saves 12s per node, and the state captures (iptables, conntrack, flows, endpoints) are the part that is actually worth taking at report time.
  2. iptables-save and ovs-ofctl dump-flows br-int have no size cap, unlike the 5000-line conntrack dump. Neither is likely to be large on the sandbox, but a head -n bound would make the report size predictable.
  3. The cozyreport comment says the 300s wrapper matches "the wrapper cozytest.sh puts around the same script". cozytest uses timeout -k 30 600, and the Chainsaw catch uses 300.
  4. The bats cases pin the predicate. Nothing pins the fact that --baseline actually issues the iptables-save exec, the only capture that is new. Main's suite already stubs kubectl and asserts argv for the other captures, so one case there would cover it.

CI on 1f6bc1be (2026-07-22T18:40Z, attempt 1): pre-commit green. E2E Tests red on chainsaw/mariadb. That failure came before the report step and has nothing to do with this diff. The run is two months old and will re-run after the rebase.

timeout 25 kubectl exec -n "$KUBEOVN_NS" "$_cni" -c cni-server -- \
sh -c 'command -v iptables-save >/dev/null 2>&1 && iptables-save || echo "DIAG-SKIP: iptables-save not present in cni-server"' \
> "$OUT/$_bn-iptables.txt" 2>&1 || true
_diag_prune "$OUT/$_bn-iptables.txt"

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.

If timeout 25 kills the exec before it writes anything, the file is empty and this prune deletes it, so a timed-out capture and a node with nothing to capture look the same on disk. I sourced the script and checked: timeout 1 sh -c "sleep 5" > f gives rc=124, and _diag_prunable f then returns true. The same applies to the four captures below. Please record the exec's exit status in a capture-notes.txt before pruning (B2).

# _diag_prunable -- the baseline-capture pruning predicate. It must drop empty #
# files and this script's own "DIAG-SKIP:" absence markers, but KEEP a genuine #
# kubectl exec failure so a broken capture stays distinguishable from an absent #
# tool (PR review blocker B2). Mock inputs use real kubectl failure text. #

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.

"(PR review blocker B2)" points at a review thread that means nothing once this is merged. The sentence before it already says why the case matters, so the reference can just go (B4).

@lexfrei

Copy link
Copy Markdown
Contributor

IvanHunters the capture script this PR changes was reworked in August, so the branch conflicts with main now. Can you rebase onto main and answer the review from 2026-09-26?

@github-actions github-actions Bot removed the lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale label Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci Issues or PRs related to CI workflows, GitHub Actions, automation area/testing Issues or PRs related to testing (e2e, bats, unit tests) size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants