Repository navigation
test(e2e): capture per-node network diagnostics in cozyreport - #3213
IvanHunters wants to merge 2 commits into
Conversation
📝 WalkthroughWalkthroughAdds a baseline per-node network diagnostics capture to ChangesNetwork Diagnostics Capture
Estimated code review effort: 3 (Moderate) | ~25 minutes Suggested reviewers: 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
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Summary of ChangesHello, 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
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 AssistThe 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
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 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
|
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
There are two issues in this conditional block:
- If
wc -lfails 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 expectedindash/sh). Assigning the line count to a variable with a default fallback (e.g.,${_lines:-0}) avoids this. - The flags
--extended-regexpand--quietare GNU-specific extensions and may not be supported on non-GNU platforms (such as macOS or Alpine/busybox environments). Using the standard POSIX flags-Eand-qis more portable and universally supported.
| 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 |
|
|
||
| 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") |
|
|
||
| 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") |
|
|
||
| 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") |
| if [ -z "$(ls -A "$DIR" 2>/dev/null)" ]; then | ||
| rmdir "$DIR" 2>/dev/null || true | ||
| fi |
There was a problem hiding this comment.
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.
| if [ -z "$(ls -A "$DIR" 2>/dev/null)" ]; then | |
| rmdir "$DIR" 2>/dev/null || true | |
| fi | |
| rmdir "$DIR" 2>/dev/null || true |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
hack/cozyreport.sh (1)
417-432: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider 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
📒 Files selected for processing (1)
hack/cozyreport.sh
| 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") |
There was a problem hiding this comment.
🩺 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.
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
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
hack/cozyreport.sh:385-387— thecommand -v timeoutguard is dead code. The only environment that runs this script is the e2e sandbox, andpackages/core/testing/images/e2e-sandbox/Dockerfile:1isFROM ubuntu:24.04, so coreutilstimeoutis always present. It is also inconsistent withe2e-capture-dataplane.shandcozytest.sh:123, which both usetimeoutunguarded. Harmless today, but if it ever fired it would silently skip the entire section on one line of output.hack/cozyreport.sh:447— the cilium selector diverges from the sibling (app.kubernetes.io/name=cilium-agenthere,k8s-app=ciliumate2e-capture-dataplane.sh:292). Both are correct; two scripts in the same pipeline should agree.hack/cozyreport.sh:388-477— theelsebody is not indented, which makes the closingfiat :478 hard to pair. An early-skip guard would read better.- 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.
| # 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"' \ |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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→ deletederror: 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.
| # 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"' \ |
There was a problem hiding this comment.
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.
| 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..." |
There was a problem hiding this comment.
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.
| fi | ||
| } | ||
|
|
||
| for pod in $(kubectl -n cozy-kubeovn get pods -l app=kube-ovn-cni -o name 2>/dev/null); do |
There was a problem hiding this comment.
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.
| # `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 |
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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).
|
IvanHunters CI is green, but this PR remains blocked by the requested-changes review. I rechecked current head
Please address these and rebase after #3421 lands because it changes the same |
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]>
4a96635 to
1f6bc1b
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (1)
hack/e2e-capture-dataplane.sh (1)
300-349: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBaseline captures duplicate existing
capture_nodelogic in the same file.
ovs-ofctl dump-flows br-int(325-331) and thecilium-dbg endpoint list/ boundedcilium-dbg monitor --type droppair (333-349) incapture_baselinere-implement near-identical logic already present incapture_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
📒 Files selected for processing (3)
hack/capture-dataplane.batshack/cozyreport.shhack/e2e-capture-dataplane.sh
myasnikovdaniil
left a comment
There was a problem hiding this comment.
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 ' ') |
There was a problem hiding this comment.
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"' \ |
There was a problem hiding this comment.
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' \ |
There was a problem hiding this comment.
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.
|
This PR has had no activity for 60 days and was marked |
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
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 dropreplaced it. - B2 (pruning deletes failures): half closed. A kubectl error line is now kept, since
_diag_prunableonly drops empty files and loneDIAG-SKIP:markers. An exec that produced nothing is still deleted, though. That is B2 below. - B3 (conntrack fallback): closed. The
/proc/net/nf_conntrackfallback is in. - B4 (aggregate bound): closed.
timeout -k 30 300wraps the call athack/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.batsgains 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.
8fb1e11fdescribes<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 -Ldumps in hash-table order, not by age, sohead -n 5000keeps an arbitrary 5000 entries.1f6bc1beopens with "Address the review on the per-node network diagnostics". That is review-cycle wording, not a reason.hack/capture-dataplane.bats:239puts "(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
- I suggested
cilium-dbg monitor --type dropin 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. iptables-saveandovs-ofctl dump-flows br-inthave no size cap, unlike the 5000-line conntrack dump. Neither is likely to be large on the sandbox, but ahead -nbound would make the report size predictable.- 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. - The bats cases pin the predicate. Nothing pins the fact that
--baselineactually issues theiptables-saveexec, 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" |
There was a problem hiding this comment.
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. # |
There was a problem hiding this comment.
"(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).
|
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? |
What this PR does
Extends
hack/cozyreport.shwith anet-diagsection that captures per-satellite-node network state from the DaemonSet pods already running on the node:iptables-save+conntrack -L(fromkube-ovn-cnicni-server),ovs-ofctl dump-flows br-int(fromovs-ovnopenvswitch), andhubble observe+cilium-dbg endpoint list(fromciliumcilium-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 withdial tcp <podIP>:9471: connect: connection refusedfor 49 s, while an identical sibling replica on another satellite runs clean. Cozyreport at end-of-job carriesCiliumEndpoint 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.shonly, +129 lines under a newnet-diagmodule 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:
app=kube-ovn-cni(ovncni-ds.yaml),app=ovs(ovsovn-ds.yaml, not accidentally matched byapp=ovs-dpdk),app.kubernetes.io/name=cilium-agent(only the agent, not operator/envoy/pre-flight/etc.).cni-server,openvswitch,cilium-agent) match the pod specs.iptables-save,conntrack,ipincni-server;ovs-ofctlinopenvswitch;hubble,cilium-dbgincilium-agent./var/run/cilium/hubble.sockmatcheshubble.socketPathin the cilium chart values.timeout 30matches the upper bound the siblinghack/e2e-capture-dataplane.shuses for heavy captures._keep_if_usefulpruning 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
Summary by CodeRabbit