# SPDX-License-Identifier: Apache-2.0 *** Settings *** Documentation End-to-end tests for ngx_http_ipng_stats_module. ... Deploys a 3-node containerlab topology and validates ... attribution, counters, histograms, filters, variables, ... and reload semantics. Library OperatingSystem Library String Suite Setup Deploy Lab Suite Teardown Cleanup Lab *** Variables *** ${lab-name} ipng-stats-test ${lab-file} lab/ipng-stats.clab.yml ${runtime} docker ${CLAB_BIN} sudo containerlab ${SERVER} clab-${lab-name}-server ${CLIENT1} clab-${lab-name}-client1 ${CLIENT2} clab-${lab-name}-client2 ${SCRAPE_URL} http://172.20.40.2:9113/.well-known/ipng/statsz ${SERVER_MGMT} http://172.20.40.2:9180 *** Test Cases *** # --- Basic functionality --- Module loads [Documentation] nginx -t passes with the module loaded. ${output} = Docker Exec ${SERVER} nginx -t 2>&1 Should Contain ${output} syntax is ok Prometheus scrape [Documentation] Scrape returns HELP/TYPE preamble. ${output} = Scrape Prometheus Should Contain ${output} nginx-ipng-stats-plugin Should Contain ${output} nginx_ipng_requests_total JSON scrape [Documentation] Accept: application/json returns valid JSON with schema. ${rc} ${output} = Run And Return Rc And Output ... curl -sf -H 'Accept: application/json' ${SCRAPE_URL} | python3 -m json.tool Should Be Equal As Integers ${rc} 0 Should Contain ${output} "schema": 2 # --- Per-device attribution --- Attribute tag1 via eth1 (v4) [Documentation] IPv4 traffic on server:eth1 carries source_tag=tag1. Send Fast Requests ${CLIENT1} 10.0.1.1 5 Wait For Flush ${output} = Scrape Prometheus Should Contain ${output} source_tag="tag1" Should Contain ${output} vip="10.0.1.1" Attribute tag2-v4 via eth2 (v4) [Documentation] IPv4 traffic on server:eth2 carries source_tag=tag2-v4. Send Fast Requests ${CLIENT2} 10.0.2.1 5 Wait For Flush ${output} = Scrape Prometheus Should Contain ${output} source_tag="tag2-v4" Should Contain ${output} vip="10.0.2.1" Attribute tag1 via eth1 (v6) [Documentation] IPv6 traffic on server:eth1 carries source_tag=tag1 ... — same tag as v4, demonstrating that tag= can be ... shared across address families for one device. Send Fast Requests v6 ${CLIENT1} 2001:db8:1::1 5 Wait For Flush ${output} = Scrape With Filter source_tag=tag1 Should Contain ${output} source_tag="tag1" Should Contain ${output} vip="2001:db8:1::1" Attribute tag2-v6 via eth2 (v6) [Documentation] IPv6 traffic on server:eth2 carries source_tag=tag2-v6 ... — distinct from the eth2 v4 tag, demonstrating ... per-(device, family) attribution. Send Fast Requests v6 ${CLIENT2} 2001:db8:2::1 5 Wait For Flush ${output} = Scrape Prometheus Should Contain ${output} source_tag="tag2-v6" Should Contain ${output} vip="2001:db8:2::1" Direct traffic tagged [Documentation] Mgmt-interface traffic carries source_tag=direct. ${rc} ${output} = Run And Return Rc And Output ... curl -sf ${SERVER_MGMT}/ Should Be Equal As Integers ${rc} 0 Wait For Flush ${output} = Scrape Prometheus Should Contain ${output} source_tag="direct" # --- Status code tracking --- Per-class code counters [Documentation] 4xx and 2xx appear as class-bucketed code= labels. Docker Exec Ignore Rc ${CLIENT1} curl -s http://10.0.1.1:8080/notfound Docker Exec Ignore Rc ${CLIENT1} curl -s http://10.0.1.1:8080/notfound Wait For Flush ${output} = Scrape With Filter source_tag=tag1 Should Contain ${output} code="4xx" Should Contain ${output} code="2xx" # --- Duration histogram --- Duration histogram [Documentation] proxy_pass to a 50 ms backend populates sum and buckets. Send Slow Requests ${CLIENT1} 10.0.1.1 3 Wait For Flush ${prom} = Scrape With Filter source_tag=tag1 Should Match Regexp ${prom} request_duration_seconds_sum\\{[^}]*\\}\\s+\\d+\\.\\d*[1-9] ${rc} ${json} = Run And Return Rc And Output ... curl -sf -H 'Accept: application/json' '${SCRAPE_URL}?source_tag=tag1' | python3 -m json.tool Should Be Equal As Integers ${rc} 0 Should Contain ${json} request_duration_ms Should Contain ${json} buckets # --- Scrape filters --- Filter by source_tag [Documentation] ?source_tag=tag1 returns tag1 only; tag2-v4 only. ${output} = Scrape With Filter source_tag=tag1 Should Contain ${output} source_tag="tag1" Should Not Contain ${output} source_tag="tag2-v4" ${output} = Scrape With Filter source_tag=tag2-v4 Should Contain ${output} source_tag="tag2-v4" Should Not Contain ${output} source_tag="tag1" Filter by VIP [Documentation] ?vip=10.0.1.1 excludes 10.0.2.1. ${output} = Scrape With Filter vip=10.0.1.1 Should Contain ${output} vip="10.0.1.1" Should Not Contain ${output} vip="10.0.2.1" Filter combined [Documentation] source_tag + vip intersection. ${output} = Scrape With Filter source_tag=tag1&vip=10.0.1.1 Should Contain ${output} source_tag="tag1" Should Contain ${output} vip="10.0.1.1" Should Not Contain ${output} source_tag="tag2-v4" Filter unknown tag [Documentation] Unknown source_tag returns empty data set. ${output} = Scrape With Filter source_tag=nonexistent Should Not Contain ${output} nginx_ipng_requests_total{ # --- nginx variable --- Variable in access log [Documentation] $ipng_source_tag appears as tag1, tag2-v4, direct in log. ${output} = Docker Exec ${SERVER} cat /var/log/nginx/access.log Should Match Regexp ${output} src=tag1 Should Match Regexp ${output} src=tag2-v4 Should Match Regexp ${output} src=direct UDP logtail [Documentation] ipng_stats_logtail udp:// sends log lines to a local ... nc listener; captured file has all sources and VIPs. ${output} = Docker Exec ${SERVER} cat /var/log/nginx/logtail-udp.log Should Match Regexp ${output} tag1 Should Match Regexp ${output} tag2-v4 Should Match Regexp ${output} direct Should Match Regexp ${output} 10\\.0\\.1\\.1 Should Match Regexp ${output} 10\\.0\\.2\\.1 # Tab-separated format Should Match Regexp ${output} \\t Logtail if= filter [Documentation] Requests to /notfound are suppressed from logtail by ... the if=$logtail_enabled condition, but still counted. ${output} = Docker Exec ${SERVER} cat /var/log/nginx/logtail-udp.log Should Not Contain ${output} /notfound # But /notfound IS in the regular access log (not filtered there). ${access} = Docker Exec ${SERVER} cat /var/log/nginx/access.log Should Contain ${access} /notfound VIP in access log [Documentation] $server_addr resolves to real IPs, not 0.0.0.0. ${output} = Docker Exec ${SERVER} cat /var/log/nginx/access.log Should Contain ${output} vip=10.0.1.1 Should Contain ${output} vip=10.0.2.1 Should Not Contain ${output} vip=0.0.0.0 # --- Reload resilience --- Counters survive reload [Documentation] Shared-memory zone persists across nginx -s reload. ${before} = Get Request Count tag1 Docker Exec ${SERVER} nginx -s reload Sleep 2s Wait for new workers ${after} = Get Request Count tag1 Should Be True ${after} >= ${before} ... Counters dropped after reload: before=${before} after=${after} Traffic after reload [Documentation] New requests are counted after reload. Send Fast Requests ${CLIENT1} 10.0.1.1 3 Wait For Flush ${output} = Scrape With Filter source_tag=tag1 Should Contain ${output} source_tag="tag1" # --- Counter correctness --- Per-(device, family) request count accuracy [Documentation] 10 requests on each of the four (device, family) ... combinations yields tag1=20, tag2-v4=10, tag2-v6=10. ... Demonstrates that one device can combine v4+v6 under ... a single tag while another device can split them. ${before_tag1} = Get Request Count tag1 ${before_tag2v4} = Get Request Count tag2-v4 ${before_tag2v6} = Get Request Count tag2-v6 Send Fast Requests ${CLIENT1} 10.0.1.1 10 Send Fast Requests v6 ${CLIENT1} 2001:db8:1::1 10 Send Fast Requests ${CLIENT2} 10.0.2.1 10 Send Fast Requests v6 ${CLIENT2} 2001:db8:2::1 10 Wait For Flush ${after_tag1} = Get Request Count tag1 ${after_tag2v4} = Get Request Count tag2-v4 ${after_tag2v6} = Get Request Count tag2-v6 ${delta_tag1} = Evaluate ${after_tag1} - ${before_tag1} ${delta_tag2v4} = Evaluate ${after_tag2v4} - ${before_tag2v4} ${delta_tag2v6} = Evaluate ${after_tag2v6} - ${before_tag2v6} Should Be Equal As Integers ${delta_tag1} 20 Should Be Equal As Integers ${delta_tag2v4} 10 Should Be Equal As Integers ${delta_tag2v6} 10 *** Keywords *** # --- Lab lifecycle --- Deploy Lab Require Deb Build Run ${CLAB_BIN} --runtime ${runtime} destroy -t ${CURDIR}/${lab-file} --cleanup 2>&1 || true ${rc} ${output} = Run And Return Rc And Output ... ${CLAB_BIN} --runtime ${runtime} deploy -t ${CURDIR}/${lab-file} Log ${output} Should Be Equal As Integers ${rc} 0 Wait Until Keyword Succeeds 90s 3s Server Is Ready Wait Until Keyword Succeeds 60s 3s Client Can Reach Server ${CLIENT1} 10.0.1.1 Wait Until Keyword Succeeds 60s 3s Client Can Reach Server ${CLIENT2} 10.0.2.1 Require Deb Build [Documentation] Fail fast with an actionable message if the user ... forgot to run `make pkg-deb` before invoking this ... suite. The server container dpkg-installs the ... built .deb via its bind-mount of build/. ${rc} ${output} = Run And Return Rc And Output ... bash -c 'ls ${EXECDIR}/build/libnginx-mod-http-ipng-stats_*.deb 2>/dev/null' Run Keyword If ${rc} != 0 ... Fail Module .deb not found — run `make pkg-deb` first. Server Is Ready ${rc} ${output} = Run And Return Rc And Output ... curl -sf ${SCRAPE_URL} Should Be Equal As Integers ${rc} 0 Client Can Reach Server [Arguments] ${client} ${server_ip} ${rc} ${output} = Run And Return Rc And Output ... docker exec ${client} curl -sf http://${server_ip}:8080/ Should Be Equal As Integers ${rc} 0 Cleanup Lab Run docker logs ${SERVER} > ${EXECDIR}/tests/out/server-docker.log 2>&1 Run docker exec ${SERVER} cat /var/log/nginx/access.log > ${EXECDIR}/tests/out/server-access.log 2>&1 Run docker exec ${SERVER} cat /var/log/nginx/error.log > ${EXECDIR}/tests/out/server-error.log 2>&1 Run docker exec ${SERVER} cat /var/log/nginx/logtail-udp.log > ${EXECDIR}/tests/out/server-logtail-udp.log 2>&1 Run docker exec ${SERVER} ip addr > ${EXECDIR}/tests/out/server-ip-addr.log 2>&1 Run docker exec ${SERVER} ip route > ${EXECDIR}/tests/out/server-ip-route.log 2>&1 Run ${CLAB_BIN} --runtime ${runtime} destroy -t ${CURDIR}/${lab-file} --cleanup # --- Traffic generation --- Send Fast Requests [Arguments] ${client} ${server_ip} ${count} FOR ${i} IN RANGE ${count} Docker Exec ${client} curl -sf http://${server_ip}:8080/ END Send Fast Requests v6 [Arguments] ${client} ${server_ip} ${count} FOR ${i} IN RANGE ${count} Docker Exec ${client} curl -sf http://[${server_ip}]:8080/ END Send Slow Requests [Arguments] ${client} ${server_ip} ${count} FOR ${i} IN RANGE ${count} Docker Exec ${client} curl -sf http://${server_ip}:8080/slow END Wait For Flush Sleep 2s # --- Scraping --- Scrape Prometheus ${rc} ${output} = Run And Return Rc And Output ... curl -sf ${SCRAPE_URL} Should Be Equal As Integers ${rc} 0 RETURN ${output} Scrape With Filter [Arguments] ${filter} ${rc} ${output} = Run And Return Rc And Output ... curl -sf '${SCRAPE_URL}?${filter}' Should Be Equal As Integers ${rc} 0 RETURN ${output} Get Request Count [Arguments] ${source} ${output} = Scrape With Filter source_tag=${source} ${matches} = Get Regexp Matches ${output} ... nginx_ipng_requests_total\\{[^}]*\\}\\s+(\\d+) 1 ${total} = Set Variable 0 FOR ${m} IN @{matches} ${total} = Evaluate ${total} + ${m} END RETURN ${total} # --- Container helpers --- Docker Exec [Arguments] ${container} ${cmd} ${rc} ${output} = Run And Return Rc And Output ... docker exec ${container} ${cmd} Should Be Equal As Integers ${rc} 0 RETURN ${output} Docker Exec Ignore Rc [Arguments] ${container} ${cmd} ${rc} ${output} = Run And Return Rc And Output ... docker exec ${container} ${cmd} RETURN ${output}