diff --git a/.azure-pipelines/build-docker-sai-test-vpp-template.yml b/.azure-pipelines/build-docker-sai-test-vpp-template.yml new file mode 100644 index 0000000000..aff620cf88 --- /dev/null +++ b/.azure-pipelines/build-docker-sai-test-vpp-template.yml @@ -0,0 +1,228 @@ +parameters: +- name: timeout + type: number + default: 60 + +- name: sairedis_artifact_name + type: string + +- name: swss_common_artifact_name + type: string + +- name: artifact_name + type: string + +- name: vpp_run_id + type: string + +jobs: +- job: BuildSaiTestVppImage + displayName: Build docker-sai-test-vpp + timeoutInMinutes: ${{ parameters.timeout }} + + pool: + vmImage: 'ubuntu-22.04' + + steps: + - checkout: self + clean: true + submodules: recursive + + - task: DownloadPipelineArtifact@2 + inputs: + artifact: ${{ parameters.sairedis_artifact_name }} + path: $(Build.ArtifactStagingDirectory)/download + patterns: | + **/libsairedis_*.deb + **/libsaivs_*.deb + **/libsaimetadata_*.deb + **/saiserverv2_*.deb + **/python-saithriftv2_*.deb + displayName: Download sonic sairedis deb packages + + - task: DownloadPipelineArtifact@2 + name: downloadSwssCommon + inputs: + source: specific + project: build + pipeline: Azure.sonic-swss-common + artifact: ${{ parameters.swss_common_artifact_name }} + path: $(Build.ArtifactStagingDirectory)/download + runVersion: latestFromBranch + runBranch: refs/heads/$(BUILD_BRANCH) + allowPartiallySucceededBuilds: true + patterns: | + **/libswsscommon_*.deb + **/libswsscommon-dev_*.deb + displayName: Download sonic swss common deb packages + + - task: DownloadPipelineArtifact@2 + name: downloadCommonLib + inputs: + source: specific + project: build + pipeline: Azure.sonic-buildimage.common_libs + artifact: common-lib + path: $(Build.ArtifactStagingDirectory)/download + runVersion: latestFromBranch + runBranch: refs/heads/$(BUILD_BRANCH) + patterns: | + **/target/debs/trixie/libyang3_*.deb + **/target/debs/trixie/libpcre3_*.deb + displayName: Download sonic-buildimage common packages + + - task: DownloadPipelineArtifact@2 + name: downloadVpp + inputs: + source: specific + project: build + pipeline: sonic-net.sonic-platform-vpp + artifact: vpp-trixie + path: $(Build.ArtifactStagingDirectory)/download + runVersion: specific + runId: ${{ parameters.vpp_run_id }} + displayName: Download resolved sonic platform-vpp packages + + - script: | + set -euxo pipefail + + download_dir="$(Build.ArtifactStagingDirectory)/download" + deb_dir="$(System.DefaultWorkingDirectory)/.azure-pipelines/docker-sai-test-vpp/debs" + context_dir="$(Build.ArtifactStagingDirectory)/docker-context" + image_tag="docker-sai-test-vpp:$(Build.DefinitionName).$(Build.BuildNumber)" + ptf_dir="$(System.DefaultWorkingDirectory)/SAI/test/ptf" + sai_revision="$(git -C SAI rev-parse HEAD)" + ptf_revision="$(git -C "$ptf_dir" rev-parse HEAD)" + ptf_describe="$(git -C "$ptf_dir" describe --tags --long --always HEAD)" + ptf_version="$(python3 .azure-pipelines/docker-sai-test-vpp/derive_ptf_version.py "$ptf_dir")" + + if [[ "$(downloadVpp.BuildNumber)" != "${{ parameters.vpp_run_id }}" ]]; then + echo "Resolved VPP run ${{ parameters.vpp_run_id }}, downloaded $(downloadVpp.BuildNumber)" >&2 + exit 1 + fi + + rm -rf "$deb_dir" + mkdir -p "$deb_dir" + + copy_one_deb() + { + local pattern="$1" + mapfile -t matches < <(find "$download_dir" -type f -name "$pattern" | sort) + if [[ "${#matches[@]}" -ne 1 ]]; then + echo "Expected exactly one $pattern package, found ${#matches[@]}" >&2 + printf ' %s\n' "${matches[@]}" >&2 + exit 1 + fi + cp -v "${matches[0]}" "$deb_dir/" + } + + copy_optional_deb() + { + local pattern="$1" + mapfile -t matches < <(find "$download_dir" -type f -name "$pattern" | sort) + if [[ "${#matches[@]}" -gt 1 ]]; then + echo "Expected at most one $pattern package, found ${#matches[@]}" >&2 + printf ' %s\n' "${matches[@]}" >&2 + exit 1 + fi + if [[ "${#matches[@]}" -eq 1 ]]; then + cp -v "${matches[0]}" "$deb_dir/" + fi + } + + copy_one_deb 'libvppinfra_*_amd64.deb' + copy_one_deb 'vpp_*_amd64.deb' + copy_one_deb 'vpp-plugin-core_*_amd64.deb' + copy_one_deb 'vpp-plugin-dpdk_*_amd64.deb' + copy_one_deb 'libsaivs_*_amd64.deb' + copy_one_deb 'libsairedis_*_amd64.deb' + copy_one_deb 'libsaimetadata_*_amd64.deb' + copy_one_deb 'saiserverv2_*_amd64.deb' + copy_one_deb 'python-saithriftv2_*_amd64.deb' + copy_one_deb 'libswsscommon_*_amd64.deb' + copy_optional_deb 'libswsscommon-dev_*_amd64.deb' + copy_one_deb 'libyang3_*_amd64.deb' + copy_optional_deb 'libpcre3_*_amd64.deb' + + provenance="$(Build.ArtifactStagingDirectory)/provenance.txt" + { + echo "build_definition=$(Build.DefinitionName)" + echo "build_number=$(Build.BuildNumber)" + echo "build_id=$(Build.BuildId)" + echo "source_version=$(Build.SourceVersion)" + echo "source_branch=$(Build.SourceBranch)" + echo "sai_revision=$sai_revision" + echo "ptf_revision=$ptf_revision" + echo "ptf_describe=$ptf_describe" + echo "ptf_version=$ptf_version" + echo "vpp_resolved_run_id=${{ parameters.vpp_run_id }}" + echo "vpp_downloaded_build=$(downloadVpp.BuildNumber)" + echo "swss_common_downloaded_build=$(downloadSwssCommon.BuildNumber)" + echo "common_lib_downloaded_build=$(downloadCommonLib.BuildNumber)" + echo + echo "packages:" + for deb in "$deb_dir"/*.deb; do + echo "File=$(basename "$deb")" + dpkg-deb -f "$deb" Package Version Architecture + sha256sum "$deb" + done + } > "$provenance" + + rm -rf "$context_dir" + context_harness="$context_dir/.azure-pipelines/docker-sai-test-vpp" + mkdir -p "$context_harness" "$context_dir/SAI/test" + for harness_file in \ + Dockerfile \ + lanemap.ini \ + port_config.ini \ + port-map.ini \ + ptf-port-map.ini \ + run_test.sh \ + sai.profile \ + swss_log_stdout_preload.cpp \ + vpp_startup.conf.template; do + cp -a ".azure-pipelines/docker-sai-test-vpp/$harness_file" "$context_harness/" + done + cp -a "$deb_dir" "$context_harness/" + cp -a SAI/test/ptf "$context_dir/SAI/test/" + cp -a SAI/test/sai_test "$context_dir/SAI/test/" + + build_args=( + --no-cache + --build-arg "PTF_VERSION=$ptf_version" + --label com.sonic.saivpp-ci=true + -f "$context_harness/Dockerfile" + -t "$image_tag" + ) + for proxy_var in http_proxy https_proxy HTTP_PROXY HTTPS_PROXY no_proxy NO_PROXY; do + if [[ -n "${!proxy_var:-}" ]]; then + build_args+=(--build-arg "$proxy_var=${!proxy_var}") + fi + done + docker build "${build_args[@]}" "$context_dir" + + echo "$image_tag" > "$(Build.ArtifactStagingDirectory)/image-tag.txt" + docker save "$image_tag" | gzip -c > "$(Build.ArtifactStagingDirectory)/docker-sai-test-vpp.gz" + contract_dir="$(Build.ArtifactStagingDirectory)/ci-contract" + mkdir -p "$contract_dir" + for contract_file in \ + ci-matrix-tests.txt \ + ci-pass-tests.txt \ + evaluate_ci_baseline.py \ + gen_compatibility_matrix.py; do + cp -v ".azure-pipelines/docker-sai-test-vpp/$contract_file" "$contract_dir/" + done + ( + cd "$(Build.ArtifactStagingDirectory)" + sha256sum \ + docker-sai-test-vpp.gz \ + image-tag.txt \ + provenance.txt \ + ci-contract/* > SHA256SUMS + ) + rm -rf "$download_dir" "$context_dir" + displayName: Build ${{ parameters.artifact_name }} + + - publish: $(Build.ArtifactStagingDirectory)/ + artifact: ${{ parameters.artifact_name }} + displayName: Archive VPP SAI test image diff --git a/.azure-pipelines/build-template.yml b/.azure-pipelines/build-template.yml index 1c5e620082..11bc003f95 100644 --- a/.azure-pipelines/build-template.yml +++ b/.azure-pipelines/build-template.yml @@ -43,12 +43,27 @@ parameters: - name: debian_version type: string +- name: saithrift_v2 + type: boolean + default: false + +- name: depends_on + type: object + default: [] + +- name: vpp_run_id + type: string + default: '' + jobs: - job: + dependsOn: ${{ parameters.depends_on }} displayName: ${{ parameters.arch }} timeoutInMinutes: ${{ parameters.timeout }} variables: DIFF_COVER_CHECK_THRESHOLD: 80 + ${{ if ne(parameters.vpp_run_id, '') }}: + VPP_RUN_ID: $[ dependencies.ResolveVpp.outputs['resolveVppRun.VPP_RUN_ID'] ] ${{ if eq(parameters.run_unit_test, true) }}: DIFF_COVER_ENABLE: 'true' @@ -187,18 +202,32 @@ jobs: sudo dpkg -i $(find ./download -name *.deb) workingDirectory: $(Build.ArtifactStagingDirectory) displayName: "Install libyang from common lib" - - task: DownloadPipelineArtifact@2 - inputs: - source: specific - project: build - pipeline: sonic-net.sonic-platform-vpp - artifact: vpp-${{ parameters.debian_version }} - runVersion: 'latestFromBranch' - runBranch: 'refs/heads/master' - allowPartiallySucceededBuilds: true - path: $(Build.ArtifactStagingDirectory)/download - displayName: "Download sonic platform-vpp deb packages" - condition: eq('${{ parameters.arch }}', 'amd64') + - ${{ if eq(parameters.vpp_run_id, '') }}: + - task: DownloadPipelineArtifact@2 + inputs: + source: specific + project: build + pipeline: sonic-net.sonic-platform-vpp + artifact: vpp-${{ parameters.debian_version }} + runVersion: 'latestFromBranch' + runBranch: 'refs/heads/master' + allowPartiallySucceededBuilds: true + path: $(Build.ArtifactStagingDirectory)/download + displayName: "Download sonic platform-vpp deb packages" + condition: eq('${{ parameters.arch }}', 'amd64') + - ${{ if ne(parameters.vpp_run_id, '') }}: + - task: DownloadPipelineArtifact@2 + inputs: + source: specific + project: build + pipeline: sonic-net.sonic-platform-vpp + artifact: vpp-${{ parameters.debian_version }} + runVersion: specific + runId: ${{ parameters.vpp_run_id }} + allowPartiallySucceededBuilds: true + path: $(Build.ArtifactStagingDirectory)/download + displayName: "Download specified sonic platform-vpp deb packages" + condition: eq('${{ parameters.arch }}', 'amd64') - script: | set -ex sudo dpkg -i download/libvppinfra-dev_*_${{ parameters.arch }}.deb @@ -273,6 +302,33 @@ jobs: gcovr -r ./ -e ".*/SAI/.*" -e ".+/json.hpp" -e "swss/.+" -e ".*/.libs/.*" -e ".*/debian/.*" -e "vslib/vpp/.*" --exclude-unreachable-branches --json-pretty -o coverage-all.json gcovr -a "coverage-*.json" -x --xml-pretty -o coverage.xml displayName: "Run sonic sairedis unit tests" + - ${{ if eq(parameters.saithrift_v2, true) }}: + - script: | + set -euxo pipefail + + sudo apt-get update + sudo apt-get install -qq -y \ + dh-exec \ + libthrift-dev \ + python3-setuptools \ + python3-thrift \ + thrift-compiler + + sudo apt-get install -y \ + ./libsaimetadata_1.0.0_${{ parameters.arch }}.deb \ + ./libsaimetadata-dev_1.0.0_${{ parameters.arch }}.deb \ + ./libsaivs_1.0.0_${{ parameters.arch }}.deb \ + ./libsaivs-dev_1.0.0_${{ parameters.arch }}.deb + + rm -f ./*saithriftv2*.deb ./saiserverv2*.deb + pushd SAI + SAITHRIFTV2=true SAITHRIFT_VER=v2 platform=vpp \ + dpkg-buildpackage -us -uc -b -j$(nproc) + popd + + test -s saiserverv2_0.9.4_${{ parameters.arch }}.deb + test -s python-saithriftv2_0.9.4_${{ parameters.arch }}.deb + displayName: "Build VPP SAI Thrift v2 packages" - publish: $(System.DefaultWorkingDirectory)/ artifact: ${{ parameters.artifact_name }} displayName: "Archive sonic sairedis debian packages" diff --git a/.azure-pipelines/docker-sai-test-vpp/.gitignore b/.azure-pipelines/docker-sai-test-vpp/.gitignore new file mode 100644 index 0000000000..2adbd7811a --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/.gitignore @@ -0,0 +1,7 @@ +# Local-only working notes and test artifacts (not published to remote). +# devdocs/ — progress/debug logs for agent/developer context across branches +# demodocs/ — stakeholder demo notes +# results/ — JUnit XML, run logs, compatibility matrices (update README pass list when changed) +devdocs/ +demodocs/ +results/ diff --git a/.azure-pipelines/docker-sai-test-vpp/Dockerfile b/.azure-pipelines/docker-sai-test-vpp/Dockerfile new file mode 100644 index 0000000000..6bbf16a11f --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/Dockerfile @@ -0,0 +1,147 @@ +FROM debian:trixie + +ENV DEBIAN_FRONTEND=noninteractive \ + PIP_BREAK_SYSTEM_PACKAGES=1 \ + PYTHONUNBUFFERED=1 \ + SAI_PROFILE=/etc/sai/sai.profile \ + SAISERVER_PORTMAP=/etc/sai/port-map.ini \ + PTF_PORTMAP=/etc/sai/ptf-port-map.ini \ + SAI_TEST_DIR=/sai_test \ + TEST_RESULTS_DIR=/test-results + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Build from the sonic-sairedis repository root: +# docker build -f .azure-pipelines/docker-sai-test-vpp/Dockerfile -t docker-sai-test-vpp . +COPY .azure-pipelines/docker-sai-test-vpp /opt/docker-sai-test-vpp +COPY SAI/test/ptf /opt/ptf +COPY SAI/test/sai_test /sai_test + +ARG PTF_VERSION + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + bash \ + build-essential \ + ca-certificates \ + iproute2 \ + libboost-serialization1.83.0 \ + libpcap0.8 \ + libthrift-0.19.0t64 \ + libzmq5 \ + procps \ + python3 \ + python3-dev \ + python3-pip \ + python3-setuptools \ + python3-thrift \ + python3-wheel \ + redis-server \ + redis-tools \ + tcpdump && \ + rm -rf /var/lib/apt/lists/* + +RUN test -n "${PTF_VERSION}" && \ + python3 -m pip install --no-cache-dir scapy==2.5.0 unittest-xml-reporting==3.2.0 && \ + SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PTF="${PTF_VERSION}" \ + python3 -m pip install --no-cache-dir --ignore-installed /opt/ptf && \ + apt-get purge -y build-essential python3-dev && \ + apt-get autoremove -y + +RUN set -eux; \ + deb_dir=/opt/docker-sai-test-vpp/debs; \ + require_deb() { \ + local label="$1"; \ + shift; \ + local found=0; \ + local pattern; \ + for pattern in "$@"; do \ + if compgen -G "${deb_dir}/${pattern}" >/dev/null; then \ + found=1; \ + fi; \ + done; \ + if [[ "$found" -eq 0 ]]; then \ + echo "Missing ${label}; expected one of: $*" >&2; \ + exit 1; \ + fi; \ + }; \ + require_deb "VPP infra package" "libvppinfra_*.deb"; \ + require_deb "VPP package" "vpp_*.deb"; \ + require_deb "VPP core plugin package" "vpp-plugin-core_*.deb"; \ + require_deb "VPP DPDK plugin package" "vpp-plugin-dpdk_*.deb"; \ + require_deb "SAI virtual switch package" "libsaivs_*.deb"; \ + require_deb "SAI Redis package" "libsairedis_*.deb"; \ + require_deb "SAI metadata package" "libsaimetadata_*.deb"; \ + require_deb "SONiC SWSS common package" "libswsscommon_*.deb"; \ + require_deb "YANG runtime package" "libyang_*.deb" "libyang3_*.deb"; \ + require_deb "SAI thrift server package" "saiserver_*.deb" "saiserverv2_*.deb"; \ + require_deb "SAI Python thrift package" "python-saithrift_*.deb" "python-saithriftv2_*.deb"; \ + runtime_debs=(); \ + have_saiserverv2=0; \ + have_python_saithriftv2=0; \ + compgen -G "${deb_dir}/saiserverv2_*.deb" >/dev/null && have_saiserverv2=1; \ + compgen -G "${deb_dir}/python-saithriftv2_*.deb" >/dev/null && have_python_saithriftv2=1; \ + while IFS= read -r deb_file; do \ + deb_name="$(basename "$deb_file")"; \ + case "$deb_name" in \ + *-dbg_*.deb|*-dbgsym_*.deb|*-dev_*.deb) continue ;; \ + libyang3-tools_*.deb) continue ;; \ + saiserver_*.deb) [[ "$have_saiserverv2" -eq 1 ]] && continue ;; \ + python-saithrift_*.deb) [[ "$have_python_saithriftv2" -eq 1 ]] && continue ;; \ + esac; \ + runtime_debs+=("$deb_file"); \ + done < <(find "$deb_dir" -maxdepth 1 -type f -name '*.deb' | sort); \ + if [[ "${#runtime_debs[@]}" -eq 0 ]]; then \ + echo "No runtime .deb packages found under ${deb_dir}" >&2; \ + exit 1; \ + fi; \ + apt-get update; \ + cp /usr/sbin/sysctl /usr/sbin/sysctl.real; \ + cp /usr/bin/true /usr/sbin/sysctl; \ + apt-get install -y --no-install-recommends --allow-downgrades "${runtime_debs[@]}"; \ + find /usr/lib/python3/dist-packages -maxdepth 1 -type d -name 'saithrift-*.egg' -print > /usr/lib/python3/dist-packages/saithrift.pth; \ + test -s /usr/lib/python3/dist-packages/saithrift.pth; \ + python3 -c 'import sai_thrift'; \ + mv /usr/sbin/sysctl.real /usr/sbin/sysctl; \ + build_dev_debs=(); \ + for f in ${deb_dir}/libswsscommon-dev_*.deb; do \ + [[ -f "$f" ]] && build_dev_debs+=("$f"); \ + done; \ + if [[ ${#build_dev_debs[@]} -gt 0 ]]; then \ + apt-get install -y --no-install-recommends "${build_dev_debs[@]}"; \ + else \ + apt-get install -y --no-install-recommends libswsscommon-dev || true; \ + fi; \ + apt-get install -y --no-install-recommends g++ libhiredis-dev && \ + g++ -shared -fPIC -o /usr/local/lib/libswss_log_stdout.so \ + /opt/docker-sai-test-vpp/swss_log_stdout_preload.cpp -lswsscommon && \ + apt-get purge -y g++ libhiredis-dev && \ + if [[ ${#build_dev_debs[@]} -gt 0 ]]; then apt-get purge -y libswsscommon-dev; fi && \ + apt-get autoremove -y && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +RUN install -d \ + /etc/sai \ + /run/vpp \ + /var/run/redis \ + /test-results \ + /usr/share/sonic/hwsku \ + /var/log && \ + install -m 0644 /opt/docker-sai-test-vpp/sai.profile /etc/sai/sai.profile && \ + install -m 0644 /opt/docker-sai-test-vpp/lanemap.ini /etc/sai/lanemap.ini && \ + install -m 0644 /opt/docker-sai-test-vpp/port_config.ini /etc/sai/port_config.ini && \ + install -m 0644 /opt/docker-sai-test-vpp/port-map.ini /etc/sai/port-map.ini && \ + install -m 0644 /opt/docker-sai-test-vpp/ptf-port-map.ini /etc/sai/ptf-port-map.ini && \ + install -m 0755 /opt/docker-sai-test-vpp/run_test.sh /usr/local/bin/run_test.sh && \ + ln -sf /usr/local/bin/run_test.sh /run_test.sh + +WORKDIR / + +# This is a single-purpose, --privileged test harness: the entrypoint starts VPP, +# Redis and saiserver, creates/destroys veth + PortChannel netdevs, and uses raw +# AF_PACKET sockets, all of which require root inside the container. It is run +# manually/in CI as a disposable test container, never as a deployed service, so a +# non-root USER would break the harness without adding meaningful isolation. +# nosemgrep: dockerfile.security.missing-user-entrypoint.missing-user-entrypoint +ENTRYPOINT ["/usr/local/bin/run_test.sh"] \ No newline at end of file diff --git a/.azure-pipelines/docker-sai-test-vpp/README.md b/.azure-pipelines/docker-sai-test-vpp/README.md new file mode 100644 index 0000000000..690d207272 --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/README.md @@ -0,0 +1,364 @@ +# docker-sai-test-vpp — VPP SAI Unit-Test Framework + +A self-contained, single-container framework for validating the **SAI API implementation of the VPP virtual-switch backend** (`libsaivs.so`) by running the OpenComputeProject (OCP) `sai_test` PTF suite against a real VPP dataplane. + +## a) Purpose and design + +### What it does + +The framework exercises SAI operations (create/set/get/remove of ports, RIFs, routes, neighbors, VLANs, FDB, LAGs, ECMP, …) through a Thrift RPC interface and verifies the resulting **data-plane** behavior by injecting and capturing packets on virtual interfaces. It is the test vehicle for finding gaps between the SAI contract and the VPP backend, and for producing a per-test **compatibility matrix**. + +### Architecture (one privileged container) + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ docker-sai-test-vpp (--privileged) │ +│ │ +│ ┌───────────┐ ┌──────────────┐ ┌────────────────────────┐ │ +│ │ VPP │ │ saiserver │ │ PTF test runner │ │ +│ │ af_packet │◄───►│ libsaivs.so │◄────►│ sai_test/*.py │ │ +│ │ linux_cp │ VPP │ (VPP SAI) │Thrift│ sai_thrift adapter │ │ +│ └─────┬─────┘ API └──────────────┘ :9092└───────────┬────────────┘ │ +│ │ AF_PACKET raw socket │ (AF_PACKET) │ +│ OEthernet0 ◄════ veth ════► OEth0_peer ──────────────┘ │ +│ … (32 pairs) │ +└──────────────────────────────────────────────────────────────────────┘ + EXISTING: VPP, libsaivs.so, sai_test, sai_thrift, PTF, af_packet/linux_cp + THIS HARNESS: Dockerfile, run_test.sh, sai.profile, *-map.ini +``` + +- **VPP** is the dataplane; `OEthernetX` are the VPP-facing kernel veth ends and `OEthX_peer` are the PTF-facing ends. `OEthernetX` represents an out-facing (wire) interface; the inside `EthernetX` (linux_cp TAP) faces the SONiC control plane. +- **saiserver** is a thin Thrift→SAI shim (port 9092) linked against `libsaivs.so`. +- **PTF** runs the OCP `sai_test` Python suite and does packet I/O on the `OEthX_peer` ends. +- **`run_test.sh`** is the container entrypoint and orchestrator: Redis → veth topology → VPP → saiserver → PTF. It writes the SONiC-to-VPP interface map to `/usr/share/sonic/hwsku/sonic_vpp_ifmap.ini`. PortChannel netdevs and LAG/SVI connected IPs are set up in sai_test `setUp()` via sai_test's `SIMULATE_SONIC` helper (`config/simulate_sonic.py`), not in `run_test.sh`. + +### Key design point — per-test isolation (default) and config-signature grouping + +The VPP SAI backend can build the switch + host-interfaces **once per saiserver process**. The OCP framework is built to configure a common T0 setup once and have subsequent tests reuse it (`common_configured=true`). But different test classes ask `T0TestBase.setUp` for *different* common configs (e.g. ECMP tests need next-hop groups), and rebuilding the full T0 config twice in one saiserver process can crash the backend. + +**Default (`ISOLATE_EACH_TEST=1`):** every requested test class runs in its **own** config group. Before each test, `run_test.sh` tears down and restarts Redis + VPP + saiserver, waits for the old VPP process to be fully reaped, then starts a fresh backend. Each test rebuilds its own common config (`common_configured=false`). This eliminates cross-test state contamination (ECMP `-6`/`-7` reuse artifacts, ordering-dependent flakes) at the cost of ~45–50 minutes for the full 4-module matrix (87 tests × ~30s recycle+rebuild each). + +**Grouped mode (`ISOLATE_EACH_TEST=0`):** `run_test.sh` parses each test class's `setUp` (resolving config kwargs through the class **inheritance chain**) to compute a common-config "signature", groups tests by signature, and for each group restarts the backend once: the first test in the group builds + persists that group's config, the rest reuse it (`common_configured=true`). This is ~9× faster but passes fewer tests when upstream workarounds are not present. + +Set `COMMON_CONFIGURED_REUSE=0` only for legacy single-invocation debugging (one ptf process, no grouping). + +### Supported tests + +The table below lists OCP `sai_test` classes that **pass** on the current VPP SAI backend (last validated **2026-07-21** against `sai_route_test sai_rif_test sai_neighbor_test sai_ecmp_test` on `sai_vpp_ut_phase3`, `ISOLATE_EACH_TEST=1`). It is the published substitute for a full compatibility matrix: only passing tests are listed. After a local matrix run, update this section when the pass set changes (see **Collecting results** below). + +| Module | Passing test classes (CI required) | +|---|---| +| `sai_ecmp_test` | `EcmpLagDisableTestV4`, `EcmpLagDisableTestV6`, `EcmpReuseLagRouteV4`, `EcmpReuseLagRouteV6`, `RemoveAllNextHopMemeberTestV4`, `RemoveNexthopGroupTestV4` | +| `sai_neighbor_test` | `AddHostRouteTest`, `AddHostRouteTestV6`, `NhopDiffPrefixRemoveLonger`, `NhopDiffPrefixRemoveLongerV6`, `NhopDiffPrefixRemoveShorter`, `NhopDiffPrefixRemoveShorterV6`, `NoHostRouteTestV6` | +| `sai_rif_test` | — | +| `sai_route_test` | `DefaultRouteV4Test`, `DefaultRouteV6Test`, `LagMultipleRouteTest`, `LagMultipleRoutev6Test`, `RemoveRouteV4Test`, `RouteDiffPrefixAddThenDeleteLongerV4Test`, `RouteDiffPrefixAddThenDeleteLongerV6Test`, `RouteDiffPrefixAddThenDeleteShorterV4Test`, `RouteDiffPrefixAddThenDeleteShorterV6Test`, `RouteRifTest`, `RouteRifv6Test`, `RouteSameSipDipv4Test`, `RouteSameSipDipv6Test`, `RouteUpdateTest`, `RouteUpdatev6Test`, `StaicSviMacFloodingTest`, `StaicSviMacFloodingV6Test` | + +**30** classes are required to pass the PR check (`ci-pass-tests.txt`). The harness plans **87** test targets across the four modules above; `gen_compatibility_matrix.py` may report a higher row count when a test produces both ERROR and FAIL JUnit entries. + +#### Flaky L3-over-LAG ECMP tests (run, not gated) + +Three additional `sai_ecmp_test` classes have passed in clean local matrix runs but are **dataplane-flaky** (hash/L3-over-LAG forwarding: pass/fail can flip run-to-run with no code change). They remain in the full matrix (`ci-matrix-tests.txt`) for visibility but are **not** in `ci-pass-tests.txt` and do not fail the PR check: + +| Module | Flaky test classes | +|---|---| +| `sai_ecmp_test` | `ReAddLagEcmpTestV4`, `RemoveLagEcmpTestV4`, `RemoveLagEcmpTestV6` | + +Promote one of these into `ci-pass-tests.txt` only after repeated clean runs show it is stable. + +### CI regression baseline + +`ci-matrix-tests.txt` is the expected set of 85 runnable selectors from the four-module plan (the 87 planned classes include two non-runnable base classes). `ci-pass-tests.txt` is the **30-selector** stable-pass subset used by the PR check. CI requires the observed JUnit selector set to match the matrix contract, then leaves failures outside the stable baseline visible while failing on a missing, failed, errored, or skipped baseline selector. + +`evaluate_ci_baseline.py` compares the JUnit directory with the baseline, reports newly passing selectors as promotion candidates, and treats missing or malformed results and harness exit codes of 2 or greater as infrastructure failures. Baseline changes are reviewed explicitly; CI never updates the file automatically. + +When a runnable test class is added, removed, or renamed in one of the four CI modules, update the sorted, fully qualified selectors in `ci-matrix-tests.txt` in the same reviewed change. + +## b) Building the framework + +The image bundles pre-built `.deb` packages from `debs/` (git-ignored locally). You only need to regenerate `.deb`s when the corresponding source changes. All examples below use the local image tag **`docker-sai-test-vpp:local`** (the `build_harness.sh` default). + +### Use cases + +The framework is designed to support three distinct deployment and testing scenarios: + +#### **Use case 1: Local dev in a `sonic-buildimage` workspace** +- **What changes:** `vslib/` C++ backend, `SAI/test/sai_test` Python tests, VPP packages, or harness scripts. +- **How dependencies are supplied:** Copy runtime `.deb`s from `target/debs//` into `docker-sai-test-vpp/debs/`, then build the image. The default suite today is **trixie** (see Dockerfile). +- **Status:** Supported today via `build_harness.sh` (below). Assumes the workspace layout `sonic-buildimage/src/sonic-sairedis`. VPP packages can come from `target/debs/trixie/` after a platform-vpp build, from a `sonic-platform-vpp` pipeline download, or from `VPP_DEB_DIR` when running the build script. + +#### **Use case 2: `sonic-sairedis` PR CI** +- **What changes:** `vslib/` C++ backend, harness files, or OCP tests. +- **How dependencies are supplied:** The pipeline's **Build** stage compiles and produces fresh `libsairedis` / `libsaivs` / `saiserver` / `python-saithrift` artifacts. Other runtime `.deb`s (`libswsscommon`, `libyang`, VPP) must be downloaded from existing pipeline artifacts — following the same pattern as `.azure-pipelines/build-docker-sonic-vs-template.yml` (swss-common pipeline, sonic-platform-vpp `vpp-trixie`, buildimage common libs). +- **VPP selection:** `BuildTrixie` resolves the latest successful VPP master run once, or uses the optional root `vpp_run_id` override. The resolved immutable run ID is used for both the compile-time VPP packages and the runtime packages installed by `BuildSaiTestVpp`. +- **Status:** Pipeline wiring is implemented by `BuildSaiTestVpp` and `TestSaiVpp`; Azure artifact authorization and burn-in are required before making the check mandatory. +- **Required runtime packages and typical artifact sources:** + +| Package glob | PR build produces? | Typical CI download source | +|---|---|---| +| `libsaivs_*`, `libsairedis_*`, `libsaimetadata_*`, `saiserverv2_*`, `python-saithriftv2_*` | Yes (this repo's Build stage) | Current pipeline job artifacts | +| `libswsscommon_*` | No | `Azure.sonic-swss-common` pipeline artifact | +| `libyang_*` | No | `sonic-buildimage` common-lib / VS build artifact | +| `libvppinfra_*`, `vpp_*`, `vpp-plugin-*` | No | `sonic-net.sonic-platform-vpp` artifact `vpp-trixie` | + +#### **Use case 3: `sonic-platform-vpp` PR CI** +- **What changes:** VPP `.deb`s only. +- **How dependencies are supplied:** Pull an approved `docker-sai-test-vpp` image and its CI contract from the `sonic-sairedis` pipeline artifact, then build a derivative image that reinstalls only the VPP runtime packages produced by the current `sonic-platform-vpp` run. +- **Status:** The producer artifact includes `ci-contract/` for the platform-vpp consumer; the consumer pipeline implementation and hosted validation are owned by `sonic-platform-vpp`. +- **Workflow:** The platform-vpp pipeline loads the approved image, overlays fresh `libvppinfra`, `vpp`, `vpp-plugin-core`, and `vpp-plugin-dpdk` packages, verifies that every non-VPP Debian package is unchanged, and runs the artifact's matrix and baseline evaluator. + +### Required `.deb` packages (validated by the Dockerfile) + +| Package glob | Source repo / how produced | +|---|---| +| `libvppinfra_*`, `vpp_*`, `vpp-plugin-core_*`, `vpp-plugin-dpdk_*` | VPP packages (from the `sonic-platform-vpp` build) | +| `libsaivs_*` | `sonic-sairedis` — the VPP SAI backend under test | +| `libsairedis_*`, `libsaimetadata_*` | `sonic-sairedis` | +| `libswsscommon_*` | `sonic-swss-common` | +| `libyang_*` / `libyang3_*` | `sonic-buildimage` (libyang3 on trixie) | +| `saiserver_*` / `saiserverv2_*` | `sonic-sairedis` SAI Thrift server | +| `python-saithrift_*` / `python-saithriftv2_*` | `sonic-sairedis` SAI Thrift Python client | + +All of these are staged in [`debs/`](debs/) (local-only). The Dockerfile installs every runtime `.deb` it finds there (skipping `-dbg`/`-dev`/`-dbgsym`). When both legacy and v2 saithrift packages are present, **`saiserverv2_*` and `python-saithriftv2_*` are preferred**. + +### Build script (use case 1) + +`build_harness.sh` automates staging from `sonic-buildimage` and building the image as **`docker-sai-test-vpp:local`**. It validates that all required runtime `.deb`s are present in `debs/` before `docker build`. If sonic-sairedis packages are missing, it runs the trixie `make` targets automatically (disable with `--no-auto-build`). VPP, `libswsscommon`, and `libyang`/`libyang3` are not auto-built — the script fails with hints if they are absent. + +```bash +cd /src/sonic-sairedis/.azure-pipelines/docker-sai-test-vpp +./build_harness.sh +``` + +Useful options: `--build-sairedis` (force-rebuild sairedis debs even when present), `--no-auto-build` (never invoke `make` for missing sairedis debs), `--no-stage-debs` (image rebuild only), `--vpp-deb-dir `, `--image-tag `. Run `./build_harness.sh --help` for the full list. + +### Regenerating the SAI `.deb`s manually (only when `vslib/` C++ changes) + +From the **buildimage repo root** (`sonic-buildimage`): + +```bash +cd + +# Force re-generation by removing the stale targets first +rm -f target/debs/trixie/libsairedis_*.deb target/debs/trixie/libsairedis-dev_*.deb \ + target/debs/trixie/libsaivs_*.deb target/debs/trixie/libsaivs-dev_*.deb + +# Build libsaivs / libsairedis +make target/debs/trixie/libsairedis_1.0.0_amd64.deb + +# Build saiserver + saithrift client (if those changed) +BLDENV=trixie make -f Makefile.work target/debs/trixie/libsaithrift-dev_0.9.4_amd64.deb + +# Stage the fresh packages into the harness build context (prefer v2 names on trixie) +cp target/debs/trixie/libsaivs_*.deb target/debs/trixie/libsaivs-dev_*.deb \ + target/debs/trixie/libsairedis_*.deb target/debs/trixie/libsairedis-dev_*.deb \ + target/debs/trixie/libsaimetadata_*.deb \ + target/debs/trixie/saiserverv2_*.deb target/debs/trixie/python-saithriftv2_*.deb \ + src/sonic-sairedis/.azure-pipelines/docker-sai-test-vpp/debs/ +``` + +Then run `./build_harness.sh` (or `./build_harness.sh --no-stage-debs` if `debs/` is already up to date). + +#### Trixie `libsaithrift-dev` build failure (workaround) + +On trixie, `make -f Makefile.work target/debs/trixie/libsaithrift-dev_0.9.4_amd64.deb` may fail (legacy `libthrift-0.11.0` / python2 debhelper dependency). `libsairedis` / `libsaivs` debs still build. If saithrift packaging fails: + +1. Build `libsairedis` as above (produces fresh `libsaivs` / `libsaimetadata`). +2. Build `saiserver` manually inside `sonic-slave-trixie` from the sairedis tree, or copy a known-good `saiserverv2_*.deb` into `debs/`. +3. Rebuild the harness image with `./build_harness.sh --no-stage-debs`. + +The image must contain a `saiserverv2` binary built against the same `libsaivs` as the staged debs — a stale saiserver deb will produce confusing Thrift/runtime failures. + +> Behind a corporate proxy, prefix `make` with your Docker build credentials, e.g. `DOCKER_CONFIG=`. VPP `.deb`s come from the `sonic-platform-vpp` pipeline; drop new ones into `debs/` or pass `--vpp-deb-dir`. `build_harness.sh` forwards proxy env vars to `docker build`. + +### Building the image manually + +Equivalent to `./build_harness.sh --no-stage-debs` after `debs/` is populated. From the **`sonic-sairedis`** directory: + +```bash +cd /src/sonic-sairedis + +docker build --no-cache \ + -f .azure-pipelines/docker-sai-test-vpp/Dockerfile \ + -t docker-sai-test-vpp:local . +``` + +If you are behind a proxy, pass it through (both lower- and upper-case): + +```bash +docker build --no-cache \ + --build-arg http_proxy=$http_proxy --build-arg https_proxy=$https_proxy \ + --build-arg HTTP_PROXY=$http_proxy --build-arg HTTPS_PROXY=$https_proxy \ + --build-arg no_proxy=$no_proxy --build-arg NO_PROXY=$no_proxy \ + -f .azure-pipelines/docker-sai-test-vpp/Dockerfile \ + -t docker-sai-test-vpp:local . +``` + +> Rebuild scope: editing only `run_test.sh` / `sai_test/**` / harness templates needs an **image rebuild** only (fast). Editing `vslib/` C++ needs a **`.deb` rebuild** (above) first, then the image. + +### VPP startup plugins (`vpp_startup.conf.template`) + +The harness-generated VPP config enables plugins required by current VPP + libsaivs, including: + +- **`tap_plugin.so`** — needed for VPP 26.06 linux-cp host-interface creation. +- **`sflow_plugin.so`** — saiserver startup asserts if the sflow message-id base is missing when this plugin is disabled. + +Other enabled plugins (`af_packet`, `linux_cp`, `linux_nl`, `acl`, `vxlan`, …) match the VPP SAI dataplane bench. Do not trim these without validating saiserver startup and host-interface creation. + +## c) Running tests + +The container entrypoint is `run_test.sh`. Test selectors are PTF targets: `module` (e.g. `sai_route_test`) or `module.Class` (e.g. `sai_route_test.RouteRifTest`). `PORT_COUNT` sets the port/veth count (use 32 for the standard T0 topology). + +### Where the tests come from + +The OCP `sai_test` suite is baked into the image at `/sai_test` (copied from `SAI/test/sai_test/` in the repo). PTF and the SAI Thrift client come from `SAI/test/ptf` and the `python-saithrift` / `python-saithriftv2` package. `run_test.sh` discovers test classes under `/sai_test` automatically. + +### Run a single test + +```bash +docker run --rm --privileged -e PORT_COUNT=32 \ + docker-sai-test-vpp:local sai_route_test.RouteRifTest +``` + +### Run several tests (one container, grouped or isolated per env) + +```bash +docker run --rm --privileged -e PORT_COUNT=32 \ + docker-sai-test-vpp:local \ + sai_route_test.RouteRifTest sai_ecmp_test.EcmpHashFieldSportTestV4 +``` + +With default `ISOLATE_EACH_TEST=1`, each selector still gets its own fresh backend even when multiple classes are listed. + +### Run whole modules, or everything + +```bash +# whole modules (default isolation: ~45-50 min for all four) +docker run --rm --privileged -e PORT_COUNT=32 \ + docker-sai-test-vpp:local sai_route_test sai_rif_test sai_neighbor_test sai_ecmp_test + +# faster grouped mode (~9x; fewer passes without upstream workarounds) +docker run --rm --privileged -e PORT_COUNT=32 -e ISOLATE_EACH_TEST=0 \ + docker-sai-test-vpp:local sai_route_test sai_rif_test sai_neighbor_test sai_ecmp_test + +# every discovered test class (no args) +docker run --rm --privileged -e PORT_COUNT=32 docker-sai-test-vpp:local +``` + +### Collecting results (JUnit XML) and updating the supported-test list + +PTF writes one JUnit-XML file per test into `/test-results`. By convention these land in the local-only `results/` tree (git-ignored; not published to the remote). Run from the `docker-sai-test-vpp/` directory and bind-mount `results/xml` out. + +For a full 4-module matrix, prefer **`nohup`** (or an equivalent detached logger) so a dropped SSH/IDE session does not kill the container mid-run. Tail the log file for progress — long runs produce megabytes of output and some IDE terminals stop updating while the run continues. + +```bash +cd /src/sonic-sairedis/.azure-pipelines/docker-sai-test-vpp +mkdir -p results/xml +rm -f results/xml/TEST-*.xml +nohup docker run --rm --privileged -e PORT_COUNT=32 \ + -v "$PWD/results/xml:/test-results" \ + docker-sai-test-vpp:local \ + sai_route_test sai_rif_test sai_neighbor_test sai_ecmp_test \ + > results/run.log 2>&1 & +tail -f results/run.log +``` + +### Monitor + +While a matrix run is in progress (from the `docker-sai-test-vpp/` directory): + +| | | +|---|---| +| **Log** | `results/run.log` (or whatever path you passed to `nohup` / `tee`) | +| **Progress** | `grep -oE '\[[0-9]+/87\]' results/run.log \| tail -1` | +| **Container** | `docker ps --filter ancestor=docker-sai-test-vpp:local` | + +The `87` in the progress grep matches the four-module isolation plan (`sai_route_test sai_rif_test sai_neighbor_test sai_ecmp_test`). For other selectors, use `grep -oE '\[[0-9]+/[0-9]+\]'` instead. + +Monitor progress without the full firehose: + +```bash +grep -oE '\[[0-9]+/[0-9]+\]' results/run.log | tail -1 +grep -E '^(OK|FAIL|ERROR) |\[run_test\] ===' results/run.log | tail -20 +``` + +Then build a local compatibility matrix with the bundled generator (defaults to the `results/` tree). The generator parses JUnit XML with `defusedxml` (hardened against XXE), so install it once if needed: + +```bash +pip install defusedxml +python3 gen_compatibility_matrix.py # writes results/compatibility-matrix.md +``` + +`gen_compatibility_matrix.py` walks `results/xml/TEST-*.xml` and writes a PASS/FAIL/ERROR/SKIP table with a count summary. You can also pass an explicit ` [output.md]` to point it elsewhere. + +Evaluate the same results against the PR baseline with the matrix process exit code (`0` for an all-pass matrix, `1` when test failures are present, or `2+` for setup failure): + +```bash +python3 evaluate_ci_baseline.py \ + --xml-dir results/xml \ + --baseline ci-pass-tests.txt \ + --expected ci-matrix-tests.txt \ + --matrix-rc 1 \ + --report results/baseline-report.txt +``` + +**Publishing pass results:** when repeated clean runs establish a newly stable pass, add its fully qualified selector to `ci-pass-tests.txt` and update the **Supported tests** section in this `README.md` in the same reviewed change. List only PASS classes and do not commit the generated matrix; working notes and deep-dive logs may be kept under `devdocs/` (also local-only and git-ignored). + +## d) Additional information + +Debug hold mode, environment variables, in-container log paths, and common pitfalls. + +### Debug mode (leave VPP / saiserver / veths alive for inspection) + +```bash +docker rm -f officesai-debug 2>/dev/null +docker run -d --name officesai-debug --privileged -e PORT_COUNT=32 \ + docker-sai-test-vpp:local --debug sai_route_test.RouteRifTest + +# while the test runs, inspect VPP state: +docker exec officesai-debug vppctl show interface +docker exec officesai-debug vppctl show ip fib +docker exec officesai-debug vppctl show bond +docker cp officesai-debug:/var/log/saiserver.log ./saiserver.log +``` + +In `--debug` the container leaves the dataplane running after the test so you can use `vppctl`; remember to `docker rm -f officesai-debug` when done. + +### Environment knobs + +| Variable | Default | Meaning | +|---|---|---| +| `PORT_COUNT` | 32 | number of `OEthernetX`/`OEthX_peer` veth pairs | +| `ISOLATE_EACH_TEST` | 1 | 1 = fresh backend + own config per test; 0 = config-signature grouping with reuse | +| `COMMON_CONFIGURED_REUSE` | 1 | 1 = plan multi-target runs with grouping/isolation; 0 = legacy single ptf invocation | +| `SAI_PORT_UP_SHARED_WAIT` | 1 | 1 = poll all ports together in `port_configer.py` (seconds, not ~64s serial) | +| `SAI_PORT_UP_RETRIES` | 2 | shared-wait retry count (with `SAI_PORT_UP_SHARED_WAIT=1`) | +| `SAI_PORT_UP_POLL_INTERVAL` | 1 | seconds between shared-wait polls | +| `LAG_COUNT` | 4 | PortChannel netdevs torn down at container exit | +| `MTU` | 9100 | veth MTU | +| `STARTUP_TIMEOUT` | 60 | seconds to wait for VPP / saiserver readiness | +| `THRIFT_PORT` | 9092 | saiserver Thrift listen port | +| `LAG_RIF_IPS` | 1 | enable LAG RIF connected-IP assignment in sai_test setUp (`SIMULATE_SONIC`) | +| `SVI_RIF_IPS` | 1 | enable SVI RIF connected-IP assignment in sai_test setUp | +| `SIMULATE_SONIC` | 1 | set by `run_test.sh`; enables sai_test's SONiC control-plane simulation (PortChannel netdevs + LAG/SVI RIF IPs) | +| `SIMULATE_SONIC_IPV6_CONTROL_SRC_MAC` | `00:77:66:55:44:00` | discard only simulated-router RS/MLDv2 startup frames from this source MAC; empty disables the filter | +| `TEST_FILTER` | — | alternative way to pass a single selector via env | + +These are read by `run_test.sh` at container start (defaults shown). + +The VPP SAI profile resolves port lane sets through `port_config.ini`. The product default is `/usr/share/sonic/hwsku/port_config.ini`; this UT profile overrides it with `/etc/sai/port_config.ini`, which is installed from the harness fixture. + +### Logs inside the container + +- `/var/log/saiserver.log` — saiserver stdout/stderr **plus** `libsaivs` `SWSS_LOG_*` lines. After the SAI change that removed `swss::Logger` setup from `saiserver.cpp`, the harness routes `SWSS_LOG_*` to **stderr** via an `LD_PRELOAD` shim (`swss_log_stdout_preload.cpp` → `/usr/local/lib/libswss_log_stdout.so`; the `.so` name is historical). `run_test.sh` redirects saiserver `2>&1` into this file so backend traces still land here. Routing to stderr (not stdout) keeps `SWSS_LOG_ENTER` lines out of `swss::exec()` captured stdout (e.g. the `find_new_bond_id` shell pipeline used during LAG create). +- `/var/log/vpp.log`, `/var/log/vpp-startup.log` — VPP CLI history / stdout (crash backtraces). +- `/var/log/vpp-api-trace.txt` — decoded VPP binary-API trace (dumped at teardown). +- `/test-results/TEST-*.xml` — per-test JUnit results (bind-mount to the local `results/xml/` directory on the host). + +### Gotchas + +- The container must be `--privileged` (raw AF_PACKET sockets on veths/TAPs). +- The VPP SAI backend can build the switch once per saiserver process; `run_test.sh` restarts the backend per test (default) or per config-signature group — do not expect to re-run a full config build inside a single long-lived saiserver. +- **`stop_backend()` waits for child exit:** `terminate_process()` calls `wait` on VPP/saiserver/redis PIDs after they die so the next `start_vpp()` cannot overlap with a dying instance (avoids transient `Killed vpp` during per-test recycle). +- **`Set port...` / `Turn up ports...` looks hung:** each test's T0 common-config build spends ~10–20s on port admin-up and veth bring-up with little console output; this is normal, not a deadlock. +- **Long matrix runs:** use `nohup` + `tail -f results/run.log`; do not rely on IDE agent terminals for live progress on 87-test isolation runs. +- A benign `buffer: numa[1] falling back to non-hugepage backed buffer pool` line at teardown is a host hugepage-availability warning, not a test failure. diff --git a/.azure-pipelines/docker-sai-test-vpp/build_harness.sh b/.azure-pipelines/docker-sai-test-vpp/build_harness.sh new file mode 100755 index 0000000000..9c5a592255 --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/build_harness.sh @@ -0,0 +1,327 @@ +#!/usr/bin/env bash +# Stage runtime .deb packages and build the docker-sai-test-vpp image. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SAIREDIS_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +BUILDIMAGE_ROOT="${SONIC_BUILDIMAGE:-$(cd "${SAIREDIS_ROOT}/../.." && pwd)}" +DEB_STAGING="${SCRIPT_DIR}/debs" +DOCKERFILE="${SAIREDIS_ROOT}/.azure-pipelines/docker-sai-test-vpp/Dockerfile" +BLDENV="${BLDENV:-trixie}" +IMAGE_TAG="${IMAGE_TAG:-docker-sai-test-vpp:local}" +STAGE_DEBS="${STAGE_DEBS:-1}" +BUILD_SAIREDIS_DEBS="${BUILD_SAIREDIS_DEBS:-0}" +AUTO_BUILD_SAIREDIS_DEBS="${AUTO_BUILD_SAIREDIS_DEBS:-1}" +VPP_DEB_DIR="${VPP_DEB_DIR:-}" +PTF_VERSION="${PTF_VERSION:-}" + +SAIREDIS_DEB_PATTERNS=( + 'libsaivs_*.deb' 'libsairedis_*.deb' 'libsaimetadata_*.deb' + 'saiserver_*.deb' 'saiserverv2_*.deb' + 'python-saithrift_*.deb' 'python-saithriftv2_*.deb' +) + +usage() +{ + cat <<'EOF' +Usage: build_harness.sh [options] + +Stage runtime .deb packages into docker-sai-test-vpp/debs/ (local-only, +git-ignored) and build the test image from the sonic-sairedis repo root. + +Options: + --no-stage-debs Skip copying .debs (use what is already in debs/) + --build-sairedis Force-rebuild libsairedis/libsaivs (+ saithrift) in + sonic-buildimage before staging + --no-auto-build Do not invoke make when sonic-sairedis .debs are + missing (validate only; fail with hints) + --bldenv Debian suite for target/debs (default: trixie) + --image-tag Docker image tag (default: docker-sai-test-vpp:local) + --vpp-deb-dir Directory of VPP .debs (default: search buildimage + target/debs/ and VPP_DEB_DIR env) + -h, --help Show this help + +Environment: + SONIC_BUILDIMAGE Path to sonic-buildimage root (auto-detected if unset) + VPP_DEB_DIR Extra directory to copy VPP .debs from + PTF_VERSION Override the version derived from SAI/test/ptf + AUTO_BUILD_SAIREDIS_DEBS 1 (default): build sairedis .debs when missing + http_proxy/https_proxy/no_proxy Passed through to docker build when set + +Examples: + # Use-case 1 (local dev): stage, auto-build sairedis if needed, build image + ./build_harness.sh + + # Force-rebuild vslib .debs, then stage + build + ./build_harness.sh --build-sairedis + + # Rebuild image only (debs/ already populated and complete) + ./build_harness.sh --no-stage-debs +EOF +} + +log() +{ + printf '[build_harness] %s\n' "$*" +} + +die() +{ + printf '[build_harness] ERROR: %s\n' "$*" >&2 + exit 1 +} + +deb_present() +{ + local dir="$1" + shift + local pattern + + [[ -d "$dir" ]] || return 1 + + for pattern in "$@"; do + shopt -s nullglob + local matches=("${dir}"/${pattern}) + shopt -u nullglob + if [[ ${#matches[@]} -gt 0 ]]; then + return 0 + fi + done + + return 1 +} + +copy_matching_debs() +{ + local src_dir="$1" + shift + local pattern + + [[ -d "$src_dir" ]] || return 0 + + for pattern in "$@"; do + shopt -s nullglob + local matches=("${src_dir}"/${pattern}) + shopt -u nullglob + if [[ "${#matches[@]}" -eq 0 ]]; then + continue + fi + cp -v "${matches[@]}" "${DEB_STAGING}/" + done +} + +stage_vpp_debs() +{ + local src_dir="$1" + copy_matching_debs "${src_dir}" \ + 'libvppinfra_*.deb' 'vpp_*.deb' 'vpp-plugin-core_*.deb' 'vpp-plugin-dpdk_*.deb' +} + +stage_sairedis_debs_from_buildimage() +{ + local deb_dir="${BUILDIMAGE_ROOT}/target/debs/${BLDENV}" + + [[ -d "${BUILDIMAGE_ROOT}" ]] || die "sonic-buildimage not found at ${BUILDIMAGE_ROOT}" + mkdir -p "${DEB_STAGING}" + + log "Staging sonic-sairedis .debs from ${deb_dir} into ${DEB_STAGING}" + copy_matching_debs "${deb_dir}" "${SAIREDIS_DEB_PATTERNS[@]}" +} + +stage_debs_from_buildimage() +{ + local deb_dir="${BUILDIMAGE_ROOT}/target/debs/${BLDENV}" + + [[ -d "${BUILDIMAGE_ROOT}" ]] || die "sonic-buildimage not found at ${BUILDIMAGE_ROOT}" + mkdir -p "${DEB_STAGING}" + + log "Staging runtime .debs from ${deb_dir} into ${DEB_STAGING}" + + stage_vpp_debs "${deb_dir}" + copy_matching_debs "${deb_dir}" \ + 'libsaivs_*.deb' 'libsairedis_*.deb' 'libsaimetadata_*.deb' \ + 'libswsscommon_*.deb' 'libswsscommon-dev_*.deb' \ + 'libyang_*.deb' 'libyang3_*.deb' 'libpcre3_*.deb' \ + 'saiserver_*.deb' 'saiserverv2_*.deb' \ + 'python-saithrift_*.deb' 'python-saithriftv2_*.deb' + + if [[ -n "${VPP_DEB_DIR}" ]]; then + log "Staging VPP .debs from VPP_DEB_DIR=${VPP_DEB_DIR}" + stage_vpp_debs "${VPP_DEB_DIR}" + fi +} + +sairedis_debs_satisfied() +{ + deb_present "${DEB_STAGING}" 'libsaivs_*.deb' \ + && deb_present "${DEB_STAGING}" 'libsairedis_*.deb' \ + && deb_present "${DEB_STAGING}" 'libsaimetadata_*.deb' \ + && deb_present "${DEB_STAGING}" 'saiserver_*.deb' 'saiserverv2_*.deb' \ + && deb_present "${DEB_STAGING}" 'python-saithrift_*.deb' 'python-saithriftv2_*.deb' +} + +build_sairedis_debs() +{ + local force="${1:-0}" + + [[ -d "${BUILDIMAGE_ROOT}" ]] || die "sonic-buildimage not found at ${BUILDIMAGE_ROOT}" + + if [[ "${force}" == "1" ]]; then + log "Force-rebuilding libsairedis/libsaivs in ${BUILDIMAGE_ROOT} (BLDENV=${BLDENV})" + ( + cd "${BUILDIMAGE_ROOT}" + rm -f "target/debs/${BLDENV}/libsairedis_"*.deb "target/debs/${BLDENV}/libsairedis-dev_"*.deb \ + "target/debs/${BLDENV}/libsaivs_"*.deb "target/debs/${BLDENV}/libsaivs-dev_"*.deb + ) + else + log "Building libsairedis/libsaivs in ${BUILDIMAGE_ROOT} (BLDENV=${BLDENV})" + fi + + ( + cd "${BUILDIMAGE_ROOT}" + if [[ "${BLDENV}" == bookworm ]]; then + NOTRIXIE=1 make "target/debs/${BLDENV}/libsairedis_1.0.0_amd64.deb" + NOTRIXIE=1 BLDENV="${BLDENV}" make -f Makefile.work \ + "target/debs/${BLDENV}/libsaithrift-dev_0.9.4_amd64.deb" + else + make "target/debs/${BLDENV}/libsairedis_1.0.0_amd64.deb" + BLDENV="${BLDENV}" make -f Makefile.work \ + "target/debs/${BLDENV}/libsaithrift-dev_0.9.4_amd64.deb" + fi + ) +} + +report_missing_debs() +{ + local -a hints=() + + if ! sairedis_debs_satisfied; then + hints+=( + "sonic-sairedis (libsaivs, libsairedis, libsaimetadata, saiserverv2, python-saithriftv2):" + " ./build_harness.sh --build-sairedis" + " (or omit --no-auto-build to build automatically when missing)" + ) + fi + + if ! deb_present "${DEB_STAGING}" 'libvppinfra_*.deb'; then + hints+=( + "VPP infra (libvppinfra_*.deb):" + " Build platform-vpp in sonic-buildimage, or download sonic-net.sonic-platform-vpp" + " pipeline artifact vpp-trixie, then re-run with --vpp-deb-dir or VPP_DEB_DIR" + ) + fi + if ! deb_present "${DEB_STAGING}" 'vpp_*.deb'; then + hints+=( + "VPP (vpp_*.deb): same sources as libvppinfra (platform-vpp build or vpp-trixie artifact)" + ) + fi + if ! deb_present "${DEB_STAGING}" 'vpp-plugin-core_*.deb'; then + hints+=( + "VPP core plugin (vpp-plugin-core_*.deb): same sources as libvppinfra" + ) + fi + if ! deb_present "${DEB_STAGING}" 'vpp-plugin-dpdk_*.deb'; then + hints+=( + "VPP DPDK plugin (vpp-plugin-dpdk_*.deb): same sources as libvppinfra" + ) + fi + if ! deb_present "${DEB_STAGING}" 'libswsscommon_*.deb'; then + hints+=( + "SONiC SWSS common (libswsscommon_*.deb):" + " From sonic-buildimage: make target/debs/${BLDENV}/libswsscommon_1.0.0_amd64.deb" + " Or download Azure.sonic-swss-common pipeline artifact, then re-run ./build_harness.sh" + ) + fi + if ! deb_present "${DEB_STAGING}" 'libyang_*.deb' 'libyang3_*.deb'; then + hints+=( + "YANG runtime (libyang_*.deb or libyang3_*.deb):" + " From sonic-buildimage: make target/debs/${BLDENV}/libyang3_3.12.2-1_amd64.deb" + " Or download sonic-buildimage.common_libs / VS build artifact, then re-run ./build_harness.sh" + ) + fi + + if [[ ${#hints[@]} -eq 0 ]]; then + return 0 + fi + + printf '[build_harness] ERROR: Missing required .debs in %s\n' "${DEB_STAGING}" >&2 + local line + for line in "${hints[@]}"; do + printf ' %s\n' "$line" >&2 + done + return 1 +} + +ensure_staged_debs() +{ + mkdir -p "${DEB_STAGING}" + + if ! sairedis_debs_satisfied && [[ "${AUTO_BUILD_SAIREDIS_DEBS}" == "1" ]] \ + && [[ "${BUILD_SAIREDIS_DEBS}" != "1" ]]; then + log "sonic-sairedis .debs missing; auto-building in sonic-buildimage" + build_sairedis_debs 0 + stage_sairedis_debs_from_buildimage + fi + + report_missing_debs || die "cannot build image until required .debs are in ${DEB_STAGING}" +} + +docker_build_args() +{ + local -a args=(docker build --no-cache -f "${DOCKERFILE}" -t "${IMAGE_TAG}" .) + local var + + args+=(--build-arg "PTF_VERSION=${PTF_VERSION}") + + for var in http_proxy https_proxy HTTP_PROXY HTTPS_PROXY no_proxy NO_PROXY; do + if [[ -n "${!var:-}" ]]; then + args+=(--build-arg "${var}=${!var}") + fi + done + + printf '%s\0' "${args[@]}" +} + +main() +{ + while [[ $# -gt 0 ]]; do + case "$1" in + --no-stage-debs) STAGE_DEBS=0 ;; + --build-sairedis) BUILD_SAIREDIS_DEBS=1 ;; + --no-auto-build) AUTO_BUILD_SAIREDIS_DEBS=0 ;; + --bldenv) shift; BLDENV="${1:?--bldenv requires a value}" ;; + --image-tag) shift; IMAGE_TAG="${1:?--image-tag requires a value}" ;; + --vpp-deb-dir) shift; VPP_DEB_DIR="${1:?--vpp-deb-dir requires a value}" ;; + -h|--help) usage; exit 0 ;; + *) die "unknown option: $1 (try --help)" ;; + esac + shift + done + + if [[ "${BUILD_SAIREDIS_DEBS}" == "1" ]]; then + build_sairedis_debs 1 + stage_sairedis_debs_from_buildimage + fi + + if [[ "${STAGE_DEBS}" == "1" ]]; then + stage_debs_from_buildimage + fi + + ensure_staged_debs + + if [[ -z "${PTF_VERSION}" ]]; then + PTF_VERSION="$(python3 "${SCRIPT_DIR}/derive_ptf_version.py" \ + "${SAIREDIS_ROOT}/SAI/test/ptf")" + fi + log "Using PTF version ${PTF_VERSION}" + + log "Building image ${IMAGE_TAG} from ${SAIREDIS_ROOT}" + ( + cd "${SAIREDIS_ROOT}" + readarray -d '' -t docker_args < <(docker_build_args) + "${docker_args[@]}" + ) + log "Done: ${IMAGE_TAG}" +} + +main "$@" diff --git a/.azure-pipelines/docker-sai-test-vpp/ci-matrix-tests.txt b/.azure-pipelines/docker-sai-test-vpp/ci-matrix-tests.txt new file mode 100644 index 0000000000..789f6d4d0f --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/ci-matrix-tests.txt @@ -0,0 +1,86 @@ +# Runnable selectors expected from the four-module SAIVPP CI matrix. +sai_ecmp_test.EcmpCoExistLagRouteV4 +sai_ecmp_test.EcmpCoExistLagRouteV6 +sai_ecmp_test.EcmpHashFieldDportTestV4 +sai_ecmp_test.EcmpHashFieldDportTestV6 +sai_ecmp_test.EcmpHashFieldProtoTestV4 +sai_ecmp_test.EcmpHashFieldProtoTestV6 +sai_ecmp_test.EcmpHashFieldSIPTestV4 +sai_ecmp_test.EcmpHashFieldSIPTestV6 +sai_ecmp_test.EcmpHashFieldSportTestV4 +sai_ecmp_test.EcmpHashFieldSportTestV6 +sai_ecmp_test.EcmpIngressDisableTestV4 +sai_ecmp_test.EcmpIngressDisableTestV6 +sai_ecmp_test.EcmpLagDisableTestV4 +sai_ecmp_test.EcmpLagDisableTestV6 +sai_ecmp_test.EcmpLagTwoLayersWithDiffHashOffsetTestV4 +sai_ecmp_test.EcmpLagTwoLayersWithDiffHashOffsetTestV6 +sai_ecmp_test.EcmpReuseLagRouteV4 +sai_ecmp_test.EcmpReuseLagRouteV6 +sai_ecmp_test.EcmpTwoLayersWithDiffHashOffsetTestV4 +sai_ecmp_test.EcmpTwoLayersWithDiffHashOffsetTestV6 +sai_ecmp_test.IngressNoDiffTestV4 +sai_ecmp_test.LagTwoLayersWithDiffHashOffsetTestV4 +sai_ecmp_test.LagTwoLayersWithDiffHashOffsetTestV6 +sai_ecmp_test.ReAddLagEcmpTestV4 +sai_ecmp_test.ReAddLagEcmpTestV6 +sai_ecmp_test.RemoveAllNextHopMemeberTestV4 +sai_ecmp_test.RemoveLagEcmpTestV4 +sai_ecmp_test.RemoveLagEcmpTestV6 +sai_ecmp_test.RemoveNexthopGroupTestV4 +sai_neighbor_test.AddHostRouteTest +sai_neighbor_test.AddHostRouteTestV6 +sai_neighbor_test.NhopDiffPrefixRemoveLonger +sai_neighbor_test.NhopDiffPrefixRemoveLongerV6 +sai_neighbor_test.NhopDiffPrefixRemoveShorter +sai_neighbor_test.NhopDiffPrefixRemoveShorterV6 +sai_neighbor_test.NoHostRouteTest +sai_neighbor_test.NoHostRouteTestV6 +sai_neighbor_test.RemoveAddNeighborTestIPV4 +sai_neighbor_test.RemoveAddNeighborTestIPV6 +sai_rif_test.IngressDisableTestV4 +sai_rif_test.IngressDisableTestV6 +sai_rif_test.IngressMacUpdateTest +sai_rif_test.IngressMacUpdateTestV6 +sai_rif_test.IngressMtuTestV4 +sai_rif_test.IngressMtuTestV6 +sai_route_test.DefaultRouteV4Test +sai_route_test.DefaultRouteV6Test +sai_route_test.DropRouteTest +sai_route_test.DropRoutev6Test +sai_route_test.LagMultipleRouteTest +sai_route_test.LagMultipleRoutev6Test +sai_route_test.RemoveRouteV4Test +sai_route_test.RouteDiffPrefixAddThenDeleteLongerV4Test +sai_route_test.RouteDiffPrefixAddThenDeleteLongerV6Test +sai_route_test.RouteDiffPrefixAddThenDeleteShorterV4Test +sai_route_test.RouteDiffPrefixAddThenDeleteShorterV6Test +sai_route_test.RouteLPMRouteNexthopTest +sai_route_test.RouteLPMRouteNexthopv6Test +sai_route_test.RouteLPMRouteRifTest +sai_route_test.RouteLPMRouteRifv6Test +sai_route_test.RouteRifTest +sai_route_test.RouteRifv6Test +sai_route_test.RouteSameSipDipv4Test +sai_route_test.RouteSameSipDipv6Test +sai_route_test.RouteUpdateTest +sai_route_test.RouteUpdatev6Test +sai_route_test.StaicSviMacFloodingTest +sai_route_test.StaicSviMacFloodingV6Test +sai_route_test.SviDirectBroadcastTest +sai_route_test.SviMacAgeAfterMoveV4Test +sai_route_test.SviMacAgeAfterMoveV6Test +sai_route_test.SviMacAgingTest +sai_route_test.SviMacAgingV6Test +sai_route_test.SviMacFloodingTest +sai_route_test.SviMacFloodingv6Test +sai_route_test.SviMacLarningAfterageV4Test +sai_route_test.SviMacLarningAfterAgeV6Test +sai_route_test.SviMacLearningTest +sai_route_test.SviMacLearningV6Test +sai_route_test.SviMacMoveV4Test +sai_route_test.SviMacMoveV6Test +sai_route_test.SviMacrMoveStressV4Test +sai_route_test.SviMacrMoveStressV6Test +sai_route_test.SviRouteL3Test +sai_route_test.SviRouteL3v6Test diff --git a/.azure-pipelines/docker-sai-test-vpp/ci-pass-tests.txt b/.azure-pipelines/docker-sai-test-vpp/ci-pass-tests.txt new file mode 100644 index 0000000000..46f06c760f --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/ci-pass-tests.txt @@ -0,0 +1,31 @@ +# Stable SAIVPP PR baseline. Keep selectors fully qualified as module.Class. +sai_ecmp_test.EcmpLagDisableTestV4 +sai_ecmp_test.EcmpLagDisableTestV6 +sai_ecmp_test.EcmpReuseLagRouteV4 +sai_ecmp_test.EcmpReuseLagRouteV6 +sai_ecmp_test.RemoveAllNextHopMemeberTestV4 +sai_ecmp_test.RemoveNexthopGroupTestV4 +sai_neighbor_test.AddHostRouteTest +sai_neighbor_test.AddHostRouteTestV6 +sai_neighbor_test.NhopDiffPrefixRemoveLonger +sai_neighbor_test.NhopDiffPrefixRemoveLongerV6 +sai_neighbor_test.NhopDiffPrefixRemoveShorter +sai_neighbor_test.NhopDiffPrefixRemoveShorterV6 +sai_neighbor_test.NoHostRouteTestV6 +sai_route_test.DefaultRouteV4Test +sai_route_test.DefaultRouteV6Test +sai_route_test.LagMultipleRouteTest +sai_route_test.LagMultipleRoutev6Test +sai_route_test.RemoveRouteV4Test +sai_route_test.RouteDiffPrefixAddThenDeleteLongerV4Test +sai_route_test.RouteDiffPrefixAddThenDeleteLongerV6Test +sai_route_test.RouteDiffPrefixAddThenDeleteShorterV4Test +sai_route_test.RouteDiffPrefixAddThenDeleteShorterV6Test +sai_route_test.RouteRifTest +sai_route_test.RouteRifv6Test +sai_route_test.RouteSameSipDipv4Test +sai_route_test.RouteSameSipDipv6Test +sai_route_test.RouteUpdateTest +sai_route_test.RouteUpdatev6Test +sai_route_test.StaicSviMacFloodingTest +sai_route_test.StaicSviMacFloodingV6Test diff --git a/.azure-pipelines/docker-sai-test-vpp/derive_ptf_version.py b/.azure-pipelines/docker-sai-test-vpp/derive_ptf_version.py new file mode 100644 index 0000000000..e701efa450 --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/derive_ptf_version.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Derive a deterministic PEP 440 version from an intact PTF checkout.""" + +import argparse +import pathlib +import re +import subprocess +import sys + + +DESCRIBE_RE = re.compile( + r"^v?(?P[0-9]+(?:\.[0-9]+)*)-" + r"(?P[0-9]+)-g(?P[0-9a-f]+)$" +) +VERSION_RE = re.compile(r"^[0-9]+(?:\.[0-9]+)*$") + + +def git(ptf_dir, *args): + result = subprocess.run( + ["git", "-C", str(ptf_dir), *args], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + return result.stdout.strip() + + +def version_from_describe(description): + match = DESCRIBE_RE.fullmatch(description) + if not match: + raise ValueError(f"unsupported git describe output: {description}") + + version = match.group("version") + distance = int(match.group("distance")) + if distance == 0: + return version + return f"{version}.post{distance}+g{match.group('revision')}" + + +def fallback_version(ptf_dir, revision): + version_file = pathlib.Path(ptf_dir) / "Version.txt" + if version_file.is_file(): + base_version = version_file.read_text(encoding="utf-8").strip() + if not VERSION_RE.fullmatch(base_version): + raise ValueError(f"invalid PTF Version.txt value: {base_version}") + return f"{base_version}+g{revision}" + + commit_count = git(ptf_dir, "rev-list", "--count", "HEAD") + return f"0.0.post{commit_count}+g{revision}" + + +def derive_version(ptf_dir): + if git(ptf_dir, "status", "--porcelain"): + raise ValueError(f"PTF checkout is dirty: {ptf_dir}") + + revision = git(ptf_dir, "rev-parse", "--short=7", "HEAD") + try: + description = git( + ptf_dir, + "describe", + "--tags", + "--long", + "--match", + "v[0-9]*", + "HEAD", + ) + except subprocess.CalledProcessError: + return fallback_version(ptf_dir, revision) + return version_from_describe(description) + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("ptf_dir", type=pathlib.Path) + args = parser.parse_args(argv) + + try: + print(derive_version(args.ptf_dir)) + except (OSError, ValueError, subprocess.CalledProcessError) as exc: + print(f"error: {exc}", file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.azure-pipelines/docker-sai-test-vpp/evaluate_ci_baseline.py b/.azure-pipelines/docker-sai-test-vpp/evaluate_ci_baseline.py new file mode 100644 index 0000000000..e2981ad03b --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/evaluate_ci_baseline.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Evaluate the stable SAIVPP baseline against PTF JUnit XML results.""" + +import argparse +import glob +import os +import sys +from collections import defaultdict + +import defusedxml.ElementTree as ET + + +STATUS_ORDER = ("ERROR", "FAIL", "SKIP", "PASS") + + +def read_baseline(path): + selectors = [] + seen = set() + with open(path, encoding="utf-8") as baseline_file: + for line_number, raw_line in enumerate(baseline_file, 1): + line = raw_line.split("#", 1)[0].strip() + if not line: + continue + if line in seen: + raise ValueError(f"duplicate baseline selector at line {line_number}: {line}") + seen.add(line) + selectors.append(line) + if not selectors: + raise ValueError(f"baseline is empty: {path}") + return set(selectors) + + +def testcase_status(testcase): + if testcase.find("error") is not None: + return "ERROR" + if testcase.find("failure") is not None: + return "FAIL" + if testcase.find("skipped") is not None: + return "SKIP" + return "PASS" + + +def collect_results(xml_dir): + xml_paths = sorted(glob.glob(os.path.join(xml_dir, "TEST-*.xml"))) + if not xml_paths: + raise ValueError(f"no TEST-*.xml files found in {xml_dir}") + + statuses = defaultdict(list) + for xml_path in xml_paths: + try: + root = ET.parse(xml_path).getroot() + except Exception as exc: + raise ValueError(f"cannot parse {xml_path}: {exc}") from exc + + suites = [root] if root.tag == "testsuite" else root.iter("testsuite") + for suite in suites: + for testcase in suite.iter("testcase"): + selector = (testcase.get("classname") or "").strip() + if not selector: + module = (suite.get("name") or "").strip() + name = (testcase.get("name") or "").strip() + selector = f"{module}.{name}" if module and name else "" + if not selector: + raise ValueError(f"testcase without selector in {xml_path}") + statuses[selector].append(testcase_status(testcase)) + + if not statuses: + raise ValueError(f"no testcases found in {xml_dir}") + return statuses + + +def aggregate_status(statuses): + for status in STATUS_ORDER: + if status in statuses: + return status + raise ValueError(f"unknown test status list: {statuses}") + + +def format_report(baseline, results, matrix_rc): + observed = {selector: aggregate_status(statuses) + for selector, statuses in results.items()} + regressions = [] + for selector in sorted(baseline): + status = observed.get(selector) + if status is None: + regressions.append((selector, "MISSING")) + elif status != "PASS": + regressions.append((selector, status)) + + candidates = sorted(selector for selector, status in observed.items() + if status == "PASS" and selector not in baseline) + nonpasses = sorted((selector, status) for selector, status in observed.items() + if status != "PASS" and selector not in baseline) + + lines = [ + "SAIVPP CI baseline evaluation", + f"Matrix exit code: {matrix_rc}", + f"Baseline selectors: {len(baseline)}", + f"Observed selectors: {len(observed)}", + f"Stable baseline passes: {len(baseline) - len(regressions)}", + f"Regressions: {len(regressions)}", + f"New pass candidates: {len(candidates)}", + f"Known non-baseline non-passes: {len(nonpasses)}", + ] + if regressions: + lines.append("Regressions:") + lines.extend(f" {selector}: {status}" for selector, status in regressions) + if candidates: + lines.append("New pass candidates:") + lines.extend(f" {selector}" for selector in candidates) + if nonpasses: + lines.append("Known non-baseline non-passes:") + lines.extend(f" {selector}: {status}" for selector, status in nonpasses) + return "\n".join(lines) + "\n", regressions + + +def validate_matrix_contract(expected, results): + observed = set(results) + missing = sorted(expected - observed) + unexpected = sorted(observed - expected) + if not missing and not unexpected: + return None + + lines = ["Matrix selector contract mismatch"] + if missing: + lines.append("Missing expected selectors:") + lines.extend(f" {selector}" for selector in missing) + if unexpected: + lines.append("Unexpected selectors:") + lines.extend(f" {selector}" for selector in unexpected) + return "\n".join(lines) + + +def parse_args(argv): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--xml-dir", required=True) + parser.add_argument("--baseline", required=True) + parser.add_argument("--expected", required=True) + parser.add_argument("--matrix-rc", type=int, default=0) + parser.add_argument("--report") + return parser.parse_args(argv) + + +def main(argv=None): + args = parse_args(argv) + try: + baseline = read_baseline(args.baseline) + expected = read_baseline(args.expected) + if not baseline <= expected: + unknown = ", ".join(sorted(baseline - expected)) + raise ValueError(f"baseline selectors are not in expected matrix: {unknown}") + if args.matrix_rc >= 2: + report = ( + "SAIVPP CI baseline evaluation\n" + f"Matrix exit code: {args.matrix_rc}\n" + "Infrastructure failure: matrix setup did not complete\n" + ) + regressions = [("", "INFRASTRUCTURE")] + else: + results = collect_results(args.xml_dir) + contract_error = validate_matrix_contract(expected, results) + if contract_error: + report = ( + "SAIVPP CI baseline evaluation\n" + f"Matrix exit code: {args.matrix_rc}\n" + f"Infrastructure failure: {contract_error}\n" + ) + regressions = [("", "INCOMPLETE")] + else: + report, regressions = format_report(baseline, results, args.matrix_rc) + except (OSError, ValueError) as exc: + report = f"SAIVPP CI baseline evaluation\nInfrastructure failure: {exc}\n" + regressions = [("", "INFRASTRUCTURE")] + + if args.report: + report_dir = os.path.dirname(os.path.abspath(args.report)) + os.makedirs(report_dir, exist_ok=True) + with open(args.report, "w", encoding="utf-8") as report_file: + report_file.write(report) + print(report, end="") + return 1 if regressions else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.azure-pipelines/docker-sai-test-vpp/gen_compatibility_matrix.py b/.azure-pipelines/docker-sai-test-vpp/gen_compatibility_matrix.py new file mode 100755 index 0000000000..af95cec542 --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/gen_compatibility_matrix.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +""" +Generate a Markdown compatibility matrix from PTF JUnit-XML results. + +The VPP SAI unit-test harness (run_test.sh) writes one JUnit-XML file per test +into its --xunit-dir (mounted out as /test-results -> a host directory). By +convention that host directory is this harness's own results tree: + + docker-sai-test-vpp/results/xml/ <- per-test TEST-*.xml + docker-sai-test-vpp/results/compatibility-matrix.md <- generated matrix + +This script walks the TEST-*.xml files and emits a PASS/FAIL/ERROR/SKIP table. + +Usage: + # use the default results tree next to this script + python3 gen_compatibility_matrix.py + + # or point at an explicit xml dir / output file + python3 gen_compatibility_matrix.py [output.md] + +Example workflow: + cd docker-sai-test-vpp + mkdir -p results/xml + docker run --rm --privileged -e PORT_COUNT=32 \ + -v "$PWD/results/xml:/test-results" \ + docker-sai-test-vpp:local \ + sai_route_test sai_rif_test sai_neighbor_test sai_ecmp_test \ + 2>&1 | tee results/run.log + python3 gen_compatibility_matrix.py # writes results/compatibility-matrix.md +""" + +import sys +import os +import glob +import datetime + +# Use defusedxml (hardened against XXE / entity-expansion attacks) to parse the +# PTF JUnit XML. Install with `pip install defusedxml` if it is not already present. +import defusedxml.ElementTree as ET + +# Default results tree lives alongside this script, under docker-sai-test-vpp/. +_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +DEFAULT_RESULTS_DIR = os.path.join(_SCRIPT_DIR, "results") +DEFAULT_XML_DIR = os.path.join(DEFAULT_RESULTS_DIR, "xml") +DEFAULT_OUTPUT = os.path.join(DEFAULT_RESULTS_DIR, "compatibility-matrix.md") + +ICON = { + "PASS": "\u2705 PASS", + "FAIL": "\u274c FAIL", + "ERROR": "\u26a0\ufe0f ERROR", + "SKIP": "\u23ed\ufe0f SKIP", +} + +# Result-status key (the icons used in the Result column). +RESULT_LEGEND = [ + ("\u2705 PASS", "test passed"), + ("\u274c FAIL", "assertion failed (e.g. expected packet not received)"), + ("\u26a0\ufe0f ERROR", "test errored out (exception / setUp / tearDown)"), + ("\u23ed\ufe0f SKIP", "test was skipped"), +] + +# SAI status codes that commonly appear in the Detail column. Values mirror +# SAI/inc/saistatus.h so a failing `status == -N` is readable at a glance. +SAI_STATUS_LEGEND = [ + ("0", "SAI_STATUS_SUCCESS"), + ("-1", "SAI_STATUS_FAILURE"), + ("-2", "SAI_STATUS_NOT_SUPPORTED"), + ("-3", "SAI_STATUS_NO_MEMORY"), + ("-4", "SAI_STATUS_INSUFFICIENT_RESOURCES"), + ("-5", "SAI_STATUS_INVALID_PARAMETER"), + ("-6", "SAI_STATUS_ITEM_ALREADY_EXISTS"), + ("-7", "SAI_STATUS_ITEM_NOT_FOUND"), + ("-8", "SAI_STATUS_BUFFER_OVERFLOW"), + ("-9", "SAI_STATUS_INVALID_PORT_NUMBER"), + ("-10", "SAI_STATUS_INVALID_PORT_MEMBER"), + ("-11", "SAI_STATUS_INVALID_VLAN_ID"), + ("-12", "SAI_STATUS_UNINITIALIZED"), + ("-13", "SAI_STATUS_TABLE_FULL"), + ("-14", "SAI_STATUS_MANDATORY_ATTRIBUTE_MISSING"), + ("-15", "SAI_STATUS_NOT_IMPLEMENTED"), + ("-16", "SAI_STATUS_ADDR_NOT_FOUND"), + ("-17", "SAI_STATUS_OBJECT_IN_USE"), +] + + +def collect_rows(xml_dir): + rows = [] + for path in sorted(glob.glob(os.path.join(xml_dir, "TEST-*.xml"))): + try: + root = ET.parse(path).getroot() + except Exception as e: # malformed XML -> surface, don't crash + rows.append(("?", os.path.basename(path), "PARSE-ERR", str(e)[:60])) + continue + suites = [root] if root.tag == "testsuite" else root.iter("testsuite") + for suite in suites: + for tc in suite.iter("testcase"): + mod = tc.get("classname", "") + name = tc.get("name", "") + fail = tc.find("failure") + err = tc.find("error") + skip = tc.find("skipped") + if err is not None: + status, detail = "ERROR", (err.get("message") or "") + elif fail is not None: + status, detail = "FAIL", (fail.get("message") or "") + elif skip is not None: + status, detail = "SKIP", (skip.get("message") or "") + else: + status, detail = "PASS", "" + detail = detail.replace("\n", " ").strip()[:80] + rows.append((mod, name, status, detail)) + rows.sort() + return rows + + +def render(rows): + counts = {} + for r in rows: + counts[r[2]] = counts.get(r[2], 0) + 1 + total = len(rows) + out = [] + out.append("# VPP SAI Compatibility Matrix\n") + generated = datetime.datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") + out.append(f"_Generated: {generated}_\n") + + # Summary table: one row per status (in a stable, meaningful order) plus a + # total, with counts and percentages. + out.append("## Summary\n") + out.append("| Result | Count | % |") + out.append("|--------|------:|----:|") + order = ["PASS", "FAIL", "ERROR", "SKIP"] + seen = set(order) + for status in order + [s for s in sorted(counts) if s not in seen]: + n = counts.get(status, 0) + if n == 0 and status in seen: + continue + pct = (100.0 * n / total) if total else 0.0 + out.append(f"| {ICON.get(status, status)} | {n} | {pct:.1f}% |") + out.append(f"| **Total** | **{total}** | **100.0%** |") + out.append("") + + # Legend: result-status icons + SAI status codes seen in the Detail column. + out.append("## Legend\n") + out.append("**Result status**\n") + out.append("| Icon | Meaning |") + out.append("|------|---------|") + for icon, meaning in RESULT_LEGEND: + out.append(f"| {icon} | {meaning} |") + out.append("") + out.append("**SAI status codes** (see `SAI/inc/saistatus.h`) \u2014 appear in the Detail column\n") + out.append("| Code | Symbol |") + out.append("|------|--------|") + for code, symbol in SAI_STATUS_LEGEND: + out.append(f"| `{code}` | `{symbol}` |") + out.append("") + + out.append("## Results\n") + out.append("| Module | Test Class | Result | Detail |") + out.append("|--------|------------|--------|--------|") + for mod, name, status, detail in rows: + out.append(f"| `{mod}` | `{name}` | {ICON.get(status, status)} | {detail} |") + return "\n".join(out) + + +def main(argv): + args = argv[1:] + if args and args[0] in ("-h", "--help"): + sys.stderr.write(__doc__) + return 0 + + xml_dir = args[0] if len(args) >= 1 else DEFAULT_XML_DIR + # output: explicit 2nd arg, else default file when using the default xml dir, + # else stdout (so piping `> file.md` still works for a custom dir). + if len(args) >= 2: + output = args[1] + elif len(args) == 0: + output = DEFAULT_OUTPUT + else: + output = None # custom xml dir, no output given -> stdout + + if not os.path.isdir(xml_dir): + sys.stderr.write( + f"error: xml dir not found: {xml_dir}\n" + f" run the harness with -v /results/xml:/test-results first.\n" + ) + return 2 + + matrix = render(collect_rows(xml_dir)) + if output: + os.makedirs(os.path.dirname(os.path.abspath(output)), exist_ok=True) + with open(output, "w", encoding="utf-8") as f: + f.write(matrix + "\n") + sys.stderr.write(f"wrote {output}\n") + else: + print(matrix) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/.azure-pipelines/docker-sai-test-vpp/lanemap.ini b/.azure-pipelines/docker-sai-test-vpp/lanemap.ini new file mode 100644 index 0000000000..b9ec948707 --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/lanemap.ini @@ -0,0 +1,34 @@ +# VPP wire-side interface to SAI lane mapping. +# Lanes match the default sai_test resources/config_db.json (Ethernet0,4,8,...,124). +OEthernet0:65,66,67,68 +OEthernet1:69,70,71,72 +OEthernet2:73,74,75,76 +OEthernet3:77,78,79,80 +OEthernet4:33,34,35,36 +OEthernet5:37,38,39,40 +OEthernet6:41,42,43,44 +OEthernet7:45,46,47,48 +OEthernet8:49,50,51,52 +OEthernet9:53,54,55,56 +OEthernet10:57,58,59,60 +OEthernet11:61,62,63,64 +OEthernet12:81,82,83,84 +OEthernet13:85,86,87,88 +OEthernet14:89,90,91,92 +OEthernet15:93,94,95,96 +OEthernet16:97,98,99,100 +OEthernet17:101,102,103,104 +OEthernet18:105,106,107,108 +OEthernet19:109,110,111,112 +OEthernet20:1,2,3,4 +OEthernet21:5,6,7,8 +OEthernet22:9,10,11,12 +OEthernet23:13,14,15,16 +OEthernet24:17,18,19,20 +OEthernet25:21,22,23,24 +OEthernet26:25,26,27,28 +OEthernet27:29,30,31,32 +OEthernet28:113,114,115,116 +OEthernet29:117,118,119,120 +OEthernet30:121,122,123,124 +OEthernet31:125,126,127,128 \ No newline at end of file diff --git a/.azure-pipelines/docker-sai-test-vpp/port-map.ini b/.azure-pipelines/docker-sai-test-vpp/port-map.ini new file mode 100644 index 0000000000..726baadac3 --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/port-map.ini @@ -0,0 +1,34 @@ +# saiserver front-panel alias to SAI lane mapping. +# Port names and lanes match the default sai_test resources/config_db.json. +Ethernet0 65,66,67,68 +Ethernet4 69,70,71,72 +Ethernet8 73,74,75,76 +Ethernet12 77,78,79,80 +Ethernet16 33,34,35,36 +Ethernet20 37,38,39,40 +Ethernet24 41,42,43,44 +Ethernet28 45,46,47,48 +Ethernet32 49,50,51,52 +Ethernet36 53,54,55,56 +Ethernet40 57,58,59,60 +Ethernet44 61,62,63,64 +Ethernet48 81,82,83,84 +Ethernet52 85,86,87,88 +Ethernet56 89,90,91,92 +Ethernet60 93,94,95,96 +Ethernet64 97,98,99,100 +Ethernet68 101,102,103,104 +Ethernet72 105,106,107,108 +Ethernet76 109,110,111,112 +Ethernet80 1,2,3,4 +Ethernet84 5,6,7,8 +Ethernet88 9,10,11,12 +Ethernet92 13,14,15,16 +Ethernet96 17,18,19,20 +Ethernet100 21,22,23,24 +Ethernet104 25,26,27,28 +Ethernet108 29,30,31,32 +Ethernet112 113,114,115,116 +Ethernet116 117,118,119,120 +Ethernet120 121,122,123,124 +Ethernet124 125,126,127,128 \ No newline at end of file diff --git a/.azure-pipelines/docker-sai-test-vpp/port_config.ini b/.azure-pipelines/docker-sai-test-vpp/port_config.ini new file mode 100644 index 0000000000..7fa38aac75 --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/port_config.ini @@ -0,0 +1,33 @@ +# name lanes alias index speed +Ethernet0 65,66,67,68 fortyGigE0/0 0 40000 +Ethernet4 69,70,71,72 fortyGigE0/4 1 40000 +Ethernet8 73,74,75,76 fortyGigE0/8 2 40000 +Ethernet12 77,78,79,80 fortyGigE0/12 3 40000 +Ethernet16 33,34,35,36 fortyGigE0/16 4 40000 +Ethernet20 37,38,39,40 fortyGigE0/20 5 40000 +Ethernet24 41,42,43,44 fortyGigE0/24 6 40000 +Ethernet28 45,46,47,48 fortyGigE0/28 7 40000 +Ethernet32 49,50,51,52 fortyGigE0/32 8 40000 +Ethernet36 53,54,55,56 fortyGigE0/36 9 40000 +Ethernet40 57,58,59,60 fortyGigE0/40 10 40000 +Ethernet44 61,62,63,64 fortyGigE0/44 11 40000 +Ethernet48 81,82,83,84 fortyGigE0/48 12 40000 +Ethernet52 85,86,87,88 fortyGigE0/52 13 40000 +Ethernet56 89,90,91,92 fortyGigE0/56 14 40000 +Ethernet60 93,94,95,96 fortyGigE0/60 15 40000 +Ethernet64 97,98,99,100 fortyGigE0/64 16 40000 +Ethernet68 101,102,103,104 fortyGigE0/68 17 40000 +Ethernet72 105,106,107,108 fortyGigE0/72 18 40000 +Ethernet76 109,110,111,112 fortyGigE0/76 19 40000 +Ethernet80 1,2,3,4 fortyGigE0/80 20 40000 +Ethernet84 5,6,7,8 fortyGigE0/84 21 40000 +Ethernet88 9,10,11,12 fortyGigE0/88 22 40000 +Ethernet92 13,14,15,16 fortyGigE0/92 23 40000 +Ethernet96 17,18,19,20 fortyGigE0/96 24 40000 +Ethernet100 21,22,23,24 fortyGigE0/100 25 40000 +Ethernet104 25,26,27,28 fortyGigE0/104 26 40000 +Ethernet108 29,30,31,32 fortyGigE0/108 27 40000 +Ethernet112 113,114,115,116 fortyGigE0/112 28 40000 +Ethernet116 117,118,119,120 fortyGigE0/116 29 40000 +Ethernet120 121,122,123,124 fortyGigE0/120 30 40000 +Ethernet124 125,126,127,128 fortyGigE0/124 31 40000 \ No newline at end of file diff --git a/.azure-pipelines/docker-sai-test-vpp/ptf-port-map.ini b/.azure-pipelines/docker-sai-test-vpp/ptf-port-map.ini new file mode 100644 index 0000000000..d5143f7c99 --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/ptf-port-map.ini @@ -0,0 +1,34 @@ +# PTF port index to SAI front-panel interface mapping. +# Port names match the default sai_test resources/config_db.json (Ethernet0,4,...,124). +0@Ethernet0 +1@Ethernet4 +2@Ethernet8 +3@Ethernet12 +4@Ethernet16 +5@Ethernet20 +6@Ethernet24 +7@Ethernet28 +8@Ethernet32 +9@Ethernet36 +10@Ethernet40 +11@Ethernet44 +12@Ethernet48 +13@Ethernet52 +14@Ethernet56 +15@Ethernet60 +16@Ethernet64 +17@Ethernet68 +18@Ethernet72 +19@Ethernet76 +20@Ethernet80 +21@Ethernet84 +22@Ethernet88 +23@Ethernet92 +24@Ethernet96 +25@Ethernet100 +26@Ethernet104 +27@Ethernet108 +28@Ethernet112 +29@Ethernet116 +30@Ethernet120 +31@Ethernet124 \ No newline at end of file diff --git a/.azure-pipelines/docker-sai-test-vpp/run_test.sh b/.azure-pipelines/docker-sai-test-vpp/run_test.sh new file mode 100755 index 0000000000..7306249874 --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/run_test.sh @@ -0,0 +1,1046 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Harness assets (e.g. vpp_startup.conf.template) live next to this script in the +# source tree and are installed under /opt/docker-sai-test-vpp in the image. When +# the image runs the script from /usr/local/bin, fall back to the install dir. +HARNESS_DIR="$SCRIPT_DIR" +[[ -f "$HARNESS_DIR/vpp_startup.conf.template" ]] || HARNESS_DIR="/opt/docker-sai-test-vpp" + +PORT_COUNT="${PORT_COUNT:-32}" +MTU="${MTU:-9100}" +# Number of PortChannel (LAG) netdevs used for cleanup at container exit. +LAG_COUNT="${LAG_COUNT:-4}" +# LAG/SVI connected-IP patterns consumed by sai_test's SIMULATE_SONIC helper +# (config/simulate_sonic.py), which assigns them in setUp when a LAG or VLAN RIF is +# created. These env vars retarget the helper's defaults to this VPP bench. +LAG_RIF_IPS="${LAG_RIF_IPS:-1}" +LAG_RIF_IPV4_PATTERN="${LAG_RIF_IPV4_PATTERN:-10.1.%d.1/24}" +LAG_RIF_IPV6_PATTERN="${LAG_RIF_IPV6_PATTERN:-fc00:1::%d:1/112}" +# VPP linux_cp host-interface (LCP tap) name prefix for a BondEthernet. +LAG_BE_TAP_PREFIX="${LAG_BE_TAP_PREFIX:-be}" + +# Assign the DUT-side connected IP for each SVI (VLAN) router interface (see +# simulate_sonic.assign_svi_rif_ips). Unlike a BondEthernet, a BVI has no linux_cp +# host-interface; the IP is set directly in VPP via vppctl once the BVI exists. +SVI_RIF_IPS="${SVI_RIF_IPS:-1}" +SVI_RIF_VLANS="${SVI_RIF_VLANS:-10:1 20:2}" +SVI_RIF_IPV4_PATTERN="${SVI_RIF_IPV4_PATTERN:-192.168.%d.1/24}" +SVI_RIF_IPV6_PATTERN="${SVI_RIF_IPV6_PATTERN:-fc02::%d:1/112}" +# VPP BVI interface name prefix for a VLAN SVI. +SVI_BVI_PREFIX="${SVI_BVI_PREFIX:-bvi}" +SAI_PROFILE="${SAI_PROFILE:-/etc/sai/sai.profile}" +SAISERVER_PORTMAP="${SAISERVER_PORTMAP:-/etc/sai/port-map.ini}" +PTF_PORTMAP="${PTF_PORTMAP:-/etc/sai/ptf-port-map.ini}" +SAI_TEST_DIR="${SAI_TEST_DIR:-/sai_test}" +TEST_RESULTS_DIR="${TEST_RESULTS_DIR:-/test-results}" +SONIC_VPP_IFMAP="${SONIC_VPP_IFMAP:-/usr/share/sonic/hwsku/sonic_vpp_ifmap.ini}" +THRIFT_PORT="${THRIFT_PORT:-9092}" +STARTUP_TIMEOUT="${STARTUP_TIMEOUT:-60}" +VPPCTL_TIMEOUT="${VPPCTL_TIMEOUT:-5}" +VPP_LOG="${VPP_LOG:-/var/log/vpp.log}" +VPP_STDOUT_LOG="${VPP_STDOUT_LOG:-/var/log/vpp-startup.log}" +VPP_API_TRACE="${VPP_API_TRACE:-/tmp/vpp-sai-api-trace.api}" +VPP_API_TRACE_TXT="${VPP_API_TRACE_TXT:-/var/log/vpp-api-trace.txt}" +SAISERVER_LOG="${SAISERVER_LOG:-/var/log/saiserver.log}" +REDIS_SOCKET="${REDIS_SOCKET:-/var/run/redis/redis.sock}" +REDIS_LOG="${REDIS_LOG:-/var/log/redis.log}" +LINKS_UP_MARKER="${LINKS_UP_MARKER:-/tmp/sai-vpp-links-up}" +LINK_UP_TRIGGER="${LINK_UP_TRIGGER:-Turn up ports...}" + +# Port admin-up wait tuning, consumed by the OCP test framework's +# turn_up_and_get_checked_ports() (SAI/test/sai_test/config/port_configer.py). +# In this VPP/veth harness the SAI port oper-status reads DOWN for the whole +# wait window (linux_cp carrier settles only after the config build), yet the +# links do come up and the dataplane works - so the framework's default +# per-port serial wait (32 ports x retries x interval ~= 64s) is pure dead time +# in every common-config build. We opt into the shared bounded wait (all ports +# polled together for at most retries x interval seconds) and a short interval, +# turning ~64s into a few seconds. These are exported so the ptf subprocess +# (and thus port_configer.py) sees them; unset = upstream/real-HW default. +export SAI_PORT_UP_SHARED_WAIT="${SAI_PORT_UP_SHARED_WAIT:-1}" +export SAI_PORT_UP_RETRIES="${SAI_PORT_UP_RETRIES:-2}" +export SAI_PORT_UP_POLL_INTERVAL="${SAI_PORT_UP_POLL_INTERVAL:-1}" + +# The SAI PTF T0 framework is designed to build the common switch configuration +# once and have subsequent tests reuse it (the persisted object IDs in +# /tmp/sai_model) by passing common_configured=true. When that path is NOT used, +# every test re-runs sai_create_switch + 32x sai_create_hostif inside the same +# long-lived saiserver/VPP process, which returns a null OID on the duplicate +# host-interface and crashes saiserver - so only the first test can run. With +# reuse enabled (default) the harness runs each test target as its own ptf +# invocation against one long-lived saiserver/VPP: the first builds + persists +# the common config (common_configured=false), the rest reuse it +# (common_configured=true). Set COMMON_CONFIGURED_REUSE=0 to fall back to a +# single ptf invocation for all targets (legacy behavior). +COMMON_CONFIGURED_REUSE="${COMMON_CONFIGURED_REUSE:-1}" + +# Per-test isolation. When set to 1 (default), every test target runs in its OWN +# config group: the backend (VPP + saiserver) is torn down and brought up fresh +# before each test, and each test rebuilds its own common config from scratch +# (common_configured=false) rather than reloading a `dut` persisted by a previous +# test. This eliminates all cross-test state contamination (the ECMP -6/-7 +# config-reuse artifacts and the v4/v6 NHG port-list aliasing), so the upstream OCP +# tests need no per-test workarounds. It is only affordable because the port-up +# wait was reduced from ~64s to ~6s (see SAI_PORT_UP_SHARED_WAIT and +# devdocs/progress-6-19.md); each test pays ~22s of recycle+rebuild. Set to 0 to +# fall back to config-signature grouping with persisted-config reuse (faster full +# runs, but tests then share a backend within a group). +ISOLATE_EACH_TEST="${ISOLATE_EACH_TEST:-1}" + +# Turn on sai_test's SONiC control-plane simulation (config/simulate_sonic.py): +# create PortChannel netdevs and assign LAG/SVI RIF IPs from the test setUp, since +# this standalone bench has no teamd / IntfMgr. The remaining vars retarget the +# helper's interface names / address patterns to this VPP bench. +export SIMULATE_SONIC=1 +export SIMULATE_SONIC_IPV6_CONTROL_SRC_MAC="${SIMULATE_SONIC_IPV6_CONTROL_SRC_MAC:-00:77:66:55:44:00}" +export LAG_RIF_IPS LAG_RIF_IPV4_PATTERN LAG_RIF_IPV6_PATTERN LAG_BE_TAP_PREFIX +export SVI_RIF_IPS SVI_RIF_VLANS SVI_RIF_IPV4_PATTERN SVI_RIF_IPV6_PATTERN SVI_BVI_PREFIX +# A VLAN SVI (BVI) has no host-interface netdev, so simulate_sonic cannot use "ip". +# Provide the VPP-specific command templates it runs to program the BVI address +# directly ({ifname}/{addr} are substituted). Keeping these here (the harness layer) +# is what lets sai_test's simulate_sonic stay backend-neutral. Note: the {ifname} +# braces can't sit inside a ${VAR:-default} expansion, so guard with a plain if. +if [[ -z "${SVI_RIF_PROBE_CMD:-}" ]]; then + SVI_RIF_PROBE_CMD="timeout ${VPPCTL_TIMEOUT} vppctl show interface {ifname}" +fi +if [[ -z "${SVI_RIF_SET_IP_CMD:-}" ]]; then + SVI_RIF_SET_IP_CMD="timeout ${VPPCTL_TIMEOUT} vppctl set interface ip address {ifname} {addr}" +fi +export SVI_RIF_PROBE_CMD SVI_RIF_SET_IP_CMD +export MTU VPPCTL_TIMEOUT + +DEBUG=0 +TEST_FILTER="${TEST_FILTER:-}" +TEST_FILTERS=() +VPP_PID="" +SAISERVER_PID="" +REDIS_PID="" +VPP_CONF="" +VPP_INIT_CLI="" + +log() +{ + echo "[run_test] $*" +} + +die() +{ + echo "[run_test] ERROR: $*" >&2 + exit 2 +} + +usage() +{ + cat <<'EOF' +Usage: run_test.sh [--debug] [TEST_FILTER ...] + +Examples: + run_test.sh + run_test.sh sai_route_test.RouteRifTest + run_test.sh sai_route_test.RouteRifTest sai_route_test.RouteRifv6Test + run_test.sh --debug sai_route_test.RouteRifTest + +Multiple TEST_FILTERs run sequentially. By default each target runs as its own +ptf invocation against one long-lived saiserver/VPP: the first builds and +persists the common config, the rest reuse it (common_configured=true). This is +required because the VPP SAI backend cannot re-create the switch/host-interfaces +twice in one process. Set COMMON_CONFIGURED_REUSE=0 to run all targets in a +single legacy invocation. + +Without any TEST_FILTER, every discovered test class under /sai_test runs (each +as its own reuse invocation). In debug mode, VPP, saiserver, and veth +interfaces are left running for vppctl inspection. +EOF +} + +parse_args() +{ + while [[ $# -gt 0 ]]; do + case "$1" in + --debug) + DEBUG=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + break + ;; + --*) + die "unknown option: $1" + ;; + *) + break + ;; + esac + done + + if [[ $# -gt 0 ]]; then + TEST_FILTERS=("$@") + fi +} + +exec_requested_shell() +{ + if [[ $# -eq 0 ]]; then + return 0 + fi + + case "$1" in + bash|sh|/bin/bash|/bin/sh) + exec "$@" + ;; + esac +} + +require_command() +{ + local command_name="$1" + + command -v "$command_name" >/dev/null 2>&1 || die "required command not found: $command_name" +} + +require_file() +{ + local file_path="$1" + + [[ -f "$file_path" ]] || die "required file not found: $file_path" +} + +vpp_interface_name() +{ + local port_index="$1" + + echo "OEthernet${port_index}" +} + +ptf_interface_name() +{ + local port_index="$1" + + echo "OEth${port_index}_peer" +} + +preflight() +{ + [[ "${EUID}" -eq 0 ]] || die "run_test.sh must run as root; use docker run --privileged" + + require_command ip + require_command vpp + require_command vppctl + require_command saiserver + require_command ptf + require_command redis-server + require_command redis-cli + require_command timeout + + require_file "$SAI_PROFILE" + require_file "$SAISERVER_PORTMAP" + require_file "$PTF_PORTMAP" + [[ -d "$SAI_TEST_DIR" ]] || die "required directory not found: $SAI_TEST_DIR" + + mkdir -p /run/vpp /var/log "$(dirname "$REDIS_SOCKET")" "$TEST_RESULTS_DIR" "$(dirname "$SONIC_VPP_IFMAP")" +} + +start_redis() +{ + log "Starting Redis on $REDIS_SOCKET" + rm -f "$REDIS_SOCKET" + + redis-server \ + --daemonize no \ + --bind 127.0.0.1 \ + --port 0 \ + --unixsocket "$REDIS_SOCKET" \ + --unixsocketperm 777 \ + --save '' \ + --appendonly no \ + --logfile "$REDIS_LOG" & + REDIS_PID="$!" + + for ((attempt = 1; attempt <= STARTUP_TIMEOUT; attempt++)); do + if [[ -n "$REDIS_PID" ]] && ! kill -0 "$REDIS_PID" >/dev/null 2>&1; then + die "Redis exited before becoming ready; see $REDIS_LOG" + fi + + if redis-cli -s "$REDIS_SOCKET" ping >/dev/null 2>&1; then + return 0 + fi + + sleep 1 + done + + die "timed out waiting for Redis; see $REDIS_LOG" +} + +delete_veths() +{ + set +e + for ((port_index = 0; port_index < PORT_COUNT; port_index++)); do + local vpp_if + local ptf_if + + vpp_if="$(vpp_interface_name "$port_index")" + ptf_if="$(ptf_interface_name "$port_index")" + + if ip link show "$vpp_if" >/dev/null 2>&1; then + ip link delete "$vpp_if" >/dev/null 2>&1 + elif ip link show "$ptf_if" >/dev/null 2>&1; then + ip link delete "$ptf_if" >/dev/null 2>&1 + fi + done + set -e +} + +disable_ipv6_autoconf() +{ + # Disable IPv6 autoconfiguration on default and all interfaces. This prevents + # the Linux kernel from automatically generating IPv6 DAD and NDP neighbor/router + # solicitation packets when interfaces are brought up. Otherwise, at scale + # (PORT_COUNT=32), VPP raw sockets are flooded with unsolicited packets causing + # level-triggered poll event starvation of CLI and binary API connections. + echo 1 > /proc/sys/net/ipv6/conf/default/disable_ipv6 2>/dev/null || true + echo 1 > /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null || true +} + +delete_portchannels() +{ + set +e + for ((lag_index = 0; lag_index < LAG_COUNT; lag_index++)); do + local pc_if="PortChannel${lag_index}" + if ip link show "$pc_if" >/dev/null 2>&1; then + ip link delete "$pc_if" >/dev/null 2>&1 + fi + done + set -e +} + +create_veths() +{ + log "Creating ${PORT_COUNT} OEthernet/OEth peer veth pair(s)" + delete_veths + rm -f "$LINKS_UP_MARKER" + + for ((port_index = 0; port_index < PORT_COUNT; port_index++)); do + local vpp_if + local ptf_if + + vpp_if="$(vpp_interface_name "$port_index")" + ptf_if="$(ptf_interface_name "$port_index")" + + ip link add "$vpp_if" type veth peer name "$ptf_if" + ip link set dev "$vpp_if" mtu "$MTU" + ip link set dev "$ptf_if" mtu "$MTU" + done +} + +bring_up_veths() +{ + if [[ -f "$LINKS_UP_MARKER" ]]; then + return 0 + fi + + log "Bringing up ${PORT_COUNT} OEthernet/OEth peer veth pair(s)" + + for ((port_index = 0; port_index < PORT_COUNT; port_index++)); do + local vpp_if + local ptf_if + + vpp_if="$(vpp_interface_name "$port_index")" + ptf_if="$(ptf_interface_name "$port_index")" + + ip link set dev "$vpp_if" up + ip link set dev "$ptf_if" up + done + + : > "$LINKS_UP_MARKER" +} + +create_sonic_vpp_ifmap() +{ + log "Writing SONiC-to-VPP interface map to $SONIC_VPP_IFMAP" + : > "$SONIC_VPP_IFMAP" + + for ((port_index = 0; port_index < PORT_COUNT; port_index++)); do + echo "Ethernet$((port_index * 4)) host-OEthernet${port_index}" >> "$SONIC_VPP_IFMAP" + done +} + +generate_vpp_init_cli() +{ + VPP_INIT_CLI="$(mktemp /tmp/vpp-sai-test-init.XXXXXX.cli)" + : > "$VPP_INIT_CLI" + + for ((port_index = 0; port_index < PORT_COUNT; port_index++)); do + echo "create host-interface name OEthernet${port_index}" >> "$VPP_INIT_CLI" + echo "set interface rx-mode host-OEthernet${port_index} interrupt" >> "$VPP_INIT_CLI" + done +} + +generate_vpp_config() +{ + VPP_CONF="$(mktemp /tmp/vpp-sai-test.XXXXXX.conf)" + local template="${HARNESS_DIR}/vpp_startup.conf.template" + local buffers_per_numa=$((PORT_COUNT * 2048)) + + sed -e "s|__VPP_LOG__|${VPP_LOG}|g" \ + -e "s|__BUFFERS_PER_NUMA__|${buffers_per_numa}|g" \ + -e "s|__VPP_INIT_CLI__|${VPP_INIT_CLI}|g" \ + "$template" > "$VPP_CONF" +} + +wait_for_vpp_ready() +{ + log "Waiting for VPP to become ready" + + for ((attempt = 1; attempt <= STARTUP_TIMEOUT; attempt++)); do + if [[ -n "$VPP_PID" ]] && ! kill -0 "$VPP_PID" >/dev/null 2>&1; then + die "VPP exited before becoming ready; see $VPP_LOG and $VPP_STDOUT_LOG" + fi + + if timeout "$VPPCTL_TIMEOUT" vppctl show version >/dev/null 2>&1; then + return 0 + fi + + sleep 1 + done + + die "timed out waiting for VPP; see $VPP_LOG and $VPP_STDOUT_LOG" +} + +verify_vpp_interfaces() +{ + log "Verifying VPP interfaces are created" + local interface_output + local port_index + local attempt + + for ((attempt = 1; attempt <= 15; attempt++)); do + local all_created=1 + interface_output="$(timeout "$VPPCTL_TIMEOUT" vppctl show interface 2>/dev/null || true)" + for ((port_index = 0; port_index < PORT_COUNT; port_index++)); do + if ! grep -q "host-OEthernet${port_index}" <<< "$interface_output"; then + all_created=0 + break + fi + done + if [[ "$all_created" -eq 1 ]]; then + return 0 + fi + sleep 1 + done + + die "VPP interfaces were not created; see $VPP_LOG and $VPP_STDOUT_LOG" +} + +start_vpp() +{ + generate_vpp_init_cli + generate_vpp_config + log "Starting VPP" + vpp -c "$VPP_CONF" > "$VPP_STDOUT_LOG" 2>&1 & + VPP_PID="$!" + + wait_for_vpp_ready + verify_vpp_interfaces +} + +wait_for_saiserver_ready() +{ + log "Waiting for saiserver Thrift endpoint on 127.0.0.1:${THRIFT_PORT}" + + for ((attempt = 1; attempt <= STARTUP_TIMEOUT; attempt++)); do + if [[ -n "$SAISERVER_PID" ]] && ! kill -0 "$SAISERVER_PID" >/dev/null 2>&1; then + die "saiserver exited before becoming ready; see $SAISERVER_LOG" + fi + + if (echo >/dev/tcp/127.0.0.1/"$THRIFT_PORT") >/dev/null 2>&1; then + return 0 + fi + + sleep 1 + done + + die "timed out waiting for saiserver; see $SAISERVER_LOG" +} + +start_saiserver() +{ + log "Starting saiserver" + # libsaivs logs through SWSS_LOG_* (libswsscommon). saiserver.cpp no longer + # configures swss::Logger after the SAI standalone cleanup; LD_PRELOAD routes + # those lines to stdout so the redirect below lands in SAISERVER_LOG. + export LD_PRELOAD="/usr/local/lib/libswss_log_stdout.so${LD_PRELOAD:+:${LD_PRELOAD}}" + saiserver -p "$SAI_PROFILE" -f "$SAISERVER_PORTMAP" > "$SAISERVER_LOG" 2>&1 & + SAISERVER_PID="$!" + + wait_for_saiserver_ready +} + +ptf_target_requires_relax() +{ + case "$1" in + sai_fdb_test.BridgePortLearnDisableTest|\ + sai_fdb_test.BroadcastNoLearnTest|\ + sai_fdb_test.FdbAgingAfterMoveTest|\ + sai_fdb_test.FdbAgingTest|\ + sai_fdb_test.FdbFlushAllDynamicTest|\ + sai_fdb_test.FdbFlushAllStaticTest|\ + sai_fdb_test.FdbFlushAllTest|\ + sai_fdb_test.FdbFlushPortDynamicTest|\ + sai_fdb_test.FdbFlushPortStaticTest|\ + sai_fdb_test.FdbFlushVlanDynamicTest|\ + sai_fdb_test.FdbFlushVlanStaticTest|\ + sai_fdb_test.MulticastNoLearnTest|\ + sai_fdb_test.NonBridgePortNoLearnTest|\ + sai_fdb_test.RemoveVlanmemberLearnTest|\ + sai_fdb_test.VlanLearnDisableTest|\ + sai_route_test.SviDirectBroadcastTest|\ + sai_sanity_test.SaiSanityTest|\ + sai_tunnel_test.SviIPInIPTunnelDecapFloodV6InV4Test|\ + sai_tunnel_test.SviIPInIPTunnelDecapFloodv4Inv4Test|\ + sai_vlan_test.ArpRequestFloodingTest|\ + sai_vlan_test.BroadcastTest|\ + sai_vlan_test.DisableMacLearningTaggedTest|\ + sai_vlan_test.DisableMacLearningUntaggedTest|\ + sai_vlan_test.TaggedVlanFloodingTest|\ + sai_vlan_test.UnTaggedVlanFloodingTest) + return 0 + ;; + esac + + return 1 +} + +build_ptf_args() +{ + # Args: + # test_target a ptf test selector (module or module.Class), or empty + # for the whole /sai_test suite. + # common_configured "true" -> reuse the previously persisted common config + # "false" -> build (and persist) the common config + # "" -> do not pass the param at all (legacy) + # xunit_dir directory PTF writes its JUnit XML into. PTF rmtree's + # this dir on startup, so it must be a private per-call + # dir (NOT a bind mount, NOT shared across invocations). + local test_target="$1" + local common_configured="$2" + local xunit_dir="$3" + local test_params="thrift_server='127.0.0.1';port_map_file='$PTF_PORTMAP'" + + if [[ -n "$common_configured" ]]; then + test_params="${test_params};common_configured='${common_configured}'" + fi + + PTF_ARGS=(--test-dir "$SAI_TEST_DIR") + + for ((port_index = 0; port_index < PORT_COUNT; port_index++)); do + PTF_ARGS+=(--interface "${port_index}@$(ptf_interface_name "$port_index")") + done + + PTF_ARGS+=(--test-params "$test_params") + PTF_ARGS+=(--xunit --xunit-dir "$xunit_dir") + + # Positive flood tests expect copies on multiple ports, but PTF's flood + # verifier consumes one copy and then rejects the remaining copies via + # verify_no_other_packets(). Relax that final check only for explicitly + # classified flood targets; applying it globally would disable negative + # packet assertions in unrelated tests. + if ptf_target_requires_relax "$test_target"; then + PTF_ARGS+=(--relax) + fi + + if [[ -n "$test_target" ]]; then + PTF_ARGS+=("$test_target") + fi +} + +# Enumerate every test class under $SAI_TEST_DIR as "module.Class" selectors so +# each can run as its own config-reuse ptf invocation. Best-effort: on any +# failure prints nothing and the caller falls back to a single invocation. +enumerate_test_classes() +{ + python3 - "$SAI_TEST_DIR" <<'PY' 2>/dev/null || true +import sys, unittest +test_dir = sys.argv[1] +try: + suite = unittest.TestLoader().discover( + test_dir, pattern="sai_*_test.py", top_level_dir=test_dir) +except Exception: + sys.exit(0) +seen = [] +def walk(s): + for t in s: + if isinstance(t, unittest.TestSuite): + walk(t) + else: + cls = t.__class__ + name = "%s.%s" % (cls.__module__, cls.__name__) + if name not in seen: + seen.append(name) +walk(suite) +for n in seen: + print(n) +PY +} + +print_debug_state() +{ + set +e + log "Linux veth interfaces" + ip -br link show type veth + + if command -v vppctl >/dev/null 2>&1 && timeout "$VPPCTL_TIMEOUT" vppctl show version >/dev/null 2>&1; then + log "VPP interfaces" + timeout "$VPPCTL_TIMEOUT" vppctl show interface + log "VPP hardware interfaces" + timeout "$VPPCTL_TIMEOUT" vppctl show hardware-interfaces + log "Saving VPP API trace to $VPP_API_TRACE" + timeout "$VPPCTL_TIMEOUT" vppctl api trace save "$(basename "$VPP_API_TRACE")" + log "Writing decoded VPP API trace to $VPP_API_TRACE_TXT" + timeout "$VPPCTL_TIMEOUT" vppctl api trace dump > "$VPP_API_TRACE_TXT" 2>&1 + fi + + log "Last 200 lines of $VPP_STDOUT_LOG" + tail -n 200 "$VPP_STDOUT_LOG" 2>/dev/null + log "Last 200 lines of $VPP_LOG" + tail -n 200 "$VPP_LOG" 2>/dev/null + log "Last 200 lines of $SAISERVER_LOG" + tail -n 200 "$SAISERVER_LOG" 2>/dev/null + log "Last 200 lines of $REDIS_LOG" + tail -n 200 "$REDIS_LOG" 2>/dev/null + set -e +} + +terminate_process() +{ + local process_name="$1" + local process_pid="$2" + + if [[ -z "$process_pid" ]]; then + return 0 + fi + + if ! kill -0 "$process_pid" >/dev/null 2>&1; then + wait "$process_pid" 2>/dev/null || true + return 0 + fi + + kill "$process_pid" >/dev/null 2>&1 || true + for ((attempt = 1; attempt <= 5; attempt++)); do + if ! kill -0 "$process_pid" >/dev/null 2>&1; then + wait "$process_pid" 2>/dev/null || true + return 0 + fi + sleep 1 + done + + log "Force stopping $process_name" + kill -9 "$process_pid" >/dev/null 2>&1 || true + wait "$process_pid" 2>/dev/null || true +} + +# In --debug mode, hold the script (PID 1) open after the tests finish so the +# container keeps running with VPP, saiserver, Redis, and the veths still up for +# interactive `vppctl` / `docker exec` inspection. Without this the entrypoint +# would exit right after the last test and Docker would stop the container, +# tearing down exactly the state debug mode is meant to preserve. Exit the +# container with `docker rm -f ` when done. +debug_hold() +{ + [[ "$DEBUG" -eq 1 ]] || return 0 + + log "Debug mode: tests complete; holding container open for inspection." + log "Inspect with: docker exec vppctl show interface" + log "Stop with: docker rm -f " + # Sleep in a loop so the process stays in PID 1 and reaps cleanly on signal. + while true; do + sleep 3600 & + wait "$!" + done +} + +cleanup() +{ + local status="$?" + + set +e + if [[ "$status" -ne 0 || "$DEBUG" -eq 1 ]]; then + print_debug_state + fi + + if [[ "$DEBUG" -eq 1 ]]; then + log "Debug mode enabled; leaving VPP, saiserver, and veth interfaces running" + return "$status" + fi + + log "Cleaning up runtime state" + terminate_process saiserver "$SAISERVER_PID" + terminate_process vpp "$VPP_PID" + terminate_process redis "$REDIS_PID" + delete_veths + delete_portchannels + [[ -n "$VPP_CONF" ]] && rm -f "$VPP_CONF" + [[ -n "$VPP_INIT_CLI" ]] && rm -f "$VPP_INIT_CLI" + set -e + + return "$status" +} + +run_ptf() +{ + local -a targets=() + + if [[ "${#TEST_FILTERS[@]}" -gt 0 ]]; then + targets=("${TEST_FILTERS[@]}") + elif [[ -n "$TEST_FILTER" ]]; then + targets=("$TEST_FILTER") + fi + + # Legacy single invocation (reuse disabled): start the backend once and run + # exactly one ptf process for whatever was requested. + if [[ "$COMMON_CONFIGURED_REUSE" != "1" ]]; then + local legacy_target="" + [[ "${#targets[@]}" -gt 0 ]] && legacy_target="${targets[0]}" + start_backend + local legacy_rc=0 + run_one_ptf "$legacy_target" "" "legacy-0" || legacy_rc="$?" + debug_hold + exit "$legacy_rc" + fi + + # Reuse mode. Plan the run as a sequence of (group_id, target) lines, grouped + # by each test's common-config signature (the kwargs it passes to + # T0TestBase.setUp). Tests that need a DIFFERENT common config than the + # group's first test cannot reuse it (e.g. ECMP tests need next-hop groups), + # so each signature gets its own group. Every group runs against a FRESHLY + # restarted backend: the first test in the group builds + persists that + # group's config (common_configured=false), the rest reuse it + # (common_configured=true). The backend restart (fresh saiserver) is what + # makes a second full config build safe — building twice in ONE saiserver + # process crashes it (the original "one test per container" bug). + local plan + if [[ "${#targets[@]}" -gt 0 ]]; then + plan="$(plan_test_groups "${targets[@]}")" + else + log "No test target given; planning all discovered test classes" + plan="$(plan_test_groups)" + fi + + # Per-test isolation: rewrite the group id of every plan line to a unique value + # so each test gets its own freshly restarted backend and rebuilds its own + # common config (common_configured=false). The signature column is preserved for + # logging. This is what lets the upstream OCP tests stay free of config-reuse + # workarounds. + if [[ "$ISOLATE_EACH_TEST" == "1" && -n "$plan" ]]; then + plan="$(printf '%s\n' "$plan" | awk -F'\t' 'NF{printf "%d\t%s\t%s\n", NR-1, $2, $3}')" + log "ISOLATE_EACH_TEST=1: each test runs in its own group (fresh backend + own config)" + fi + + if [[ -z "$plan" ]]; then + log "Planning produced no targets; running full suite in one invocation" + start_backend + local suite_rc=0 + run_one_ptf "" "false" "suite-0" || suite_rc="$?" + debug_hold + exit "$suite_rc" + fi + + local total + total="$(printf '%s\n' "$plan" | grep -c .)" + log "Planned ${total} test target(s) across $(printf '%s\n' "$plan" | cut -f1 | sort -u | grep -c .) config group(s)" + + local overall_rc=0 + local prev_group="" + local idx_in_group=0 + local idx=0 + local group target sig common_configured rc + + while IFS=$'\t' read -r group target sig; do + [[ -z "$target" ]] && continue + idx=$((idx + 1)) + + if [[ "$group" != "$prev_group" ]]; then + # New config group: restart the backend for a clean saiserver, then + # the first test of the group rebuilds the common config. + if [[ -n "$prev_group" ]]; then + stop_backend + fi + log "### Config group ${group} (signature: ${sig:-()}) ###" + start_backend + prev_group="$group" + idx_in_group=0 + fi + + if [[ "$idx_in_group" -eq 0 ]]; then + common_configured="false" + else + common_configured="true" + fi + idx_in_group=$((idx_in_group + 1)) + + log "=== [${idx}/${total}] ${target} (group ${group}, common_configured=${common_configured}) ===" + rc=0 + run_one_ptf "$target" "$common_configured" "group-${group}-test-${idx}" || rc="$?" + if [[ "$rc" -ne 0 ]]; then + if (( rc > overall_rc )); then + overall_rc="$rc" + fi + log "test target '${target}' returned rc=${rc}" + fi + done <<< "$plan" + + debug_hold + exit "$overall_rc" +} + +# Start the runtime backend (Redis + VPP + saiserver). VPP and +# saiserver are fresh processes each time this is called, so a subsequent group's +# common_configured=false config build runs in a clean saiserver and does not hit +# the duplicate-create crash. Veth netdevs persist across backend restarts; +# PortChannel netdevs are created on demand in sai_test setUp (SIMULATE_SONIC). +# across restarts; only the dataplane daemons are recycled. +start_backend() +{ + start_redis + start_vpp + start_saiserver +} + +# Stop the runtime backend and reset per-run state so the next group starts clean. +# +# NOTE: ideally each test would restore the initial state via SAI teardown in its +# own tearDown(), avoiding this full backend recycle. That does not fully work yet +# because the VS-VPP backend's object removal is not idempotent enough to rebuild +# the whole T0 config a second time in one saiserver process: on remove, leftover +# VPP state (linux-cp pairs, bridge domains, bonds, etc.) is not always torn down, +# so the next create hits "already exists" (VPP VALUE_EXIST/-81) and fails. The +# host-interface case was fixed in sonic-sairedis PR #1952 (vs_remove_hostif now +# deletes the linux-cp pair + disables IPv6, and create tolerates VALUE_EXIST), but +# VLAN, bridge-port, LAG, RIF and route removal still need the same treatment. +# Until that is done, we restart the backend (fresh saiserver) per group so each +# config build starts from clean VPP state. See devdocs/progress-7-3-hostif-removal.md. +stop_backend() +{ + terminate_process saiserver "$SAISERVER_PID"; SAISERVER_PID="" + terminate_process vpp "$VPP_PID"; VPP_PID="" + terminate_process redis "$REDIS_PID"; REDIS_PID="" + # Drop persisted SAI object IDs and the link-up marker so the next group's + # first test rebuilds (and re-persists) its own config and brings up veths. + rm -rf /tmp/sai_model 2>/dev/null || true + rm -f "$LINKS_UP_MARKER" 2>/dev/null || true +} + +# Emit a run plan: tab-separated "\t\t" lines, +# grouped by each test class's common-config signature so identical-config tests +# run together (and can reuse one config build). With no args, plans every +# discovered test class under $SAI_TEST_DIR. +plan_test_groups() +{ + SAI_TEST_DIR="$SAI_TEST_DIR" python3 - "$@" <<'PY' +import ast, os, sys, glob, collections + +test_dir = os.environ.get("SAI_TEST_DIR", "/sai_test") +targets = sys.argv[1:] + +# kwargs that do NOT change the common config (so they must not split groups) +NON_CONFIG_KW = {"skip_reason", "wait_sec"} + +mod_file = {} +for p in glob.glob(os.path.join(test_dir, "sai_*_test.py")): + mod_file[os.path.basename(p)[:-3]] = p + +# Build a cross-file class registry: name -> {bases:[...], setup:FunctionDef|None, +# module:str}. Classes can subclass intermediate test bases (e.g. EcmpBaseTestV4) +# whose setUp sets the common-config kwargs, so signatures must resolve through +# the inheritance chain, not just the class's own setUp. +registry = {} +mod_classes = collections.OrderedDict() +for mod in sorted(mod_file): + try: + tree = ast.parse(open(mod_file[mod]).read(), mod_file[mod]) + except Exception: + continue + names = [] + for node in tree.body: + if not isinstance(node, ast.ClassDef): + continue + names.append(node.name) + bases = [b.id for b in node.bases if isinstance(b, ast.Name)] + setup = next((m for m in node.body + if isinstance(m, ast.FunctionDef) and m.name == "setUp"), None) + # last definition wins if duplicated + registry[node.name] = {"bases": bases, "setup": setup, "module": mod} + mod_classes[mod] = names + +def _kwargs_from_setup_call(setup): + """Return (kwargs_str_or_None, base_to_recurse_or_None) for a class's setUp. + kwargs_str: config signature if this setUp passes config kwargs. + base_to_recurse: if setUp only chains to super()/Base.setUp() without config + kwargs, the class name to resolve the signature from.""" + for sub in ast.walk(setup): + if not (isinstance(sub, ast.Call) and isinstance(sub.func, ast.Attribute) + and sub.func.attr == "setUp"): + continue + kws = [] + for kw in sub.keywords: + if kw.arg in NON_CONFIG_KW or kw.arg is None: + continue + try: + val = ast.literal_eval(kw.value) + except Exception: + val = "?" + kws.append("%s=%s" % (kw.arg, val)) + if kws: + return ("|".join(sorted(kws)), None) + # No config kwargs: figure out which base this chains to. + f = sub.func.value + if isinstance(f, ast.Call) and isinstance(f.func, ast.Name) \ + and f.func.id == "super": + return (None, "__super__") # super().setUp() + if isinstance(f, ast.Name): + return (None, f.id) # .setUp(self, ...) + return (None, "__super__") + return ("()", None) + +def signature(clsname, seen=None): + if seen is None: + seen = set() + if clsname in seen or clsname not in registry: + return "()" # unknown base (e.g. T0TestBase) -> default config + seen.add(clsname) + info = registry[clsname] + setup = info["setup"] + if setup is None: + # inherits setUp wholesale from first base + for b in info["bases"]: + return signature(b, seen) + return "()" + sig, base = _kwargs_from_setup_call(setup) + if sig is not None: + return sig + # chained to super/base without config kwargs -> resolve that base + if base == "__super__": + for b in info["bases"]: + return signature(b, seen) + return "()" + return signature(base, seen) + +# Expand requested targets (or everything) into (module, class) pairs. +pairs = [] +if not targets: + for mod in mod_classes: + for c in mod_classes[mod]: + pairs.append((mod, c)) +else: + for t in targets: + if "." in t: + mod, cls = t.split(".", 1) + pairs.append((mod, cls)) + elif t in mod_classes: + for c in mod_classes[t]: + pairs.append((t, c)) + else: + pairs.append((t, "")) # unknown selector: run as-is, default group + +# Group by resolved signature, preserving first-seen order. +groups = collections.OrderedDict() +for mod, cls in pairs: + sig = signature(cls) if cls else "()" + groups.setdefault(sig, []).append("%s.%s" % (mod, cls) if cls else mod) + +for gid, (sig, tlist) in enumerate(groups.items()): + for t in tlist: + sys.stdout.write("%d\t%s\t%s\n" % (gid, t, sig)) +PY +} + +# Copy one PTF invocation's JUnit XML into the shared results directory without +# replacing an earlier invocation's same-named TEST-.xml report. +collect_xunit_results() +{ + local xunit_dir="$1" + local invocation_namespace="$2" + local xml_file + local xml_name + local namespaced_name + local -a xml_files + + [[ -n "$TEST_RESULTS_DIR" ]] || return 0 + + mkdir -p "$TEST_RESULTS_DIR" + shopt -s nullglob + xml_files=("$xunit_dir"/*.xml) + shopt -u nullglob + + for xml_file in "${xml_files[@]}"; do + xml_name="$(basename "$xml_file")" + if [[ "$xml_name" == TEST-* ]]; then + namespaced_name="TEST-${invocation_namespace}-${xml_name#TEST-}" + else + namespaced_name="TEST-${invocation_namespace}-${xml_name}" + fi + cp "$xml_file" "$TEST_RESULTS_DIR/$namespaced_name" + done +} + +# Run a single ptf invocation for one target. Echoes PTF output through and +# triggers the one-time veth bring-up on the framework's "Turn up ports..." +# marker. Returns ptf's exit code. +# +# PTF rmtree's its --xunit-dir on startup, so each invocation gets its own +# private temp dir (which PTF may freely wipe), and the resulting JUnit XML is +# copied into the shared $TEST_RESULTS_DIR afterward. This keeps results from +# accumulating across invocations and avoids EBUSY when $TEST_RESULTS_DIR is a +# bind mount (rmtree of a mount point fails). +run_one_ptf() +{ + local test_target="$1" + local common_configured="$2" + local invocation_namespace="$3" + local test_rc + local xunit_dir + + xunit_dir="$(mktemp -d /tmp/ptf-xunit.XXXXXX)" + + build_ptf_args "$test_target" "$common_configured" "$xunit_dir" + log "Running PTF${test_target:+ filter: $test_target}" + + set +e + PYTHONUNBUFFERED=1 ptf "${PTF_ARGS[@]}" 2>&1 | while IFS= read -r ptf_line; do + printf '%s\n' "$ptf_line" + + case "$ptf_line" in + *"Turn up ports..."*) + bring_up_veths + ;; + esac + done + test_rc="${PIPESTATUS[0]}" + set -e + + collect_xunit_results "$xunit_dir" "$invocation_namespace" + rm -rf "$xunit_dir" + + return "$test_rc" +} + +main() +{ + exec_requested_shell "$@" + parse_args "$@" + trap cleanup EXIT + + preflight + disable_ipv6_autoconf + create_veths + create_sonic_vpp_ifmap + run_ptf +} + +main "$@" \ No newline at end of file diff --git a/.azure-pipelines/docker-sai-test-vpp/sai.profile b/.azure-pipelines/docker-sai-test-vpp/sai.profile new file mode 100644 index 0000000000..a74596150f --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/sai.profile @@ -0,0 +1,4 @@ +SAI_VS_SWITCH_TYPE=SAI_VS_SWITCH_TYPE_VPP +SAI_VS_HOSTIF_USE_TAP_DEVICE=true +SAI_VS_INTERFACE_LANE_MAP_FILE=/etc/sai/lanemap.ini +SAI_VS_PORT_CONFIG_FILE=/etc/sai/port_config.ini \ No newline at end of file diff --git a/.azure-pipelines/docker-sai-test-vpp/swss_log_stdout_preload.cpp b/.azure-pipelines/docker-sai-test-vpp/swss_log_stdout_preload.cpp new file mode 100644 index 0000000000..57425561de --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/swss_log_stdout_preload.cpp @@ -0,0 +1,18 @@ +// Harness-only LD_PRELOAD shim: route SAI VS library SWSS_LOG_* to stdout so +// run_test.sh capture in /var/log/sai-server.log works without pulling SONiC +// logger setup back into the SAI server binary. +#include + +namespace { + +__attribute__((constructor)) +static void route_swss_log_to_stdout() +{ + SWSS_LOG_ENTER(); + swss::Logger::setMinPrio(swss::Logger::SWSS_DEBUG); + // Standard error keeps function entry and exit logs out of captured standard + // output. The harness redirects both streams to the SAI server log. + swss::Logger::swssOutputNotify("saiserver", "STDERR"); +} + +} // namespace diff --git a/.azure-pipelines/docker-sai-test-vpp/test_derive_ptf_version.py b/.azure-pipelines/docker-sai-test-vpp/test_derive_ptf_version.py new file mode 100644 index 0000000000..d1ef8df371 --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/test_derive_ptf_version.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +import importlib.util +import os +import tempfile +import unittest +from unittest import mock + + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +MODULE_PATH = os.path.join(SCRIPT_DIR, "derive_ptf_version.py") +SPEC = importlib.util.spec_from_file_location("derive_ptf_version", MODULE_PATH) +VERSION = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(VERSION) + + +class DerivePtfVersionTest(unittest.TestCase): + def test_release_tag(self): + self.assertEqual("0.12.1", VERSION.version_from_describe("v0.12.1-0-gabc1234")) + + def test_post_release(self): + self.assertEqual( + "0.12.1.post7+gd587084", + VERSION.version_from_describe("v0.12.1-7-gd587084"), + ) + + def test_invalid_description(self): + with self.assertRaisesRegex(ValueError, "unsupported git describe"): + VERSION.version_from_describe("d587084") + + def test_version_file_fallback(self): + with tempfile.TemporaryDirectory() as temp_dir: + with open(os.path.join(temp_dir, "Version.txt"), "w", encoding="utf-8") as version_file: + version_file.write("0.12.1\n") + self.assertEqual( + "0.12.1+gd587084", + VERSION.fallback_version(temp_dir, "d587084"), + ) + + def test_untagged_fallback(self): + with tempfile.TemporaryDirectory() as temp_dir: + with mock.patch.object(VERSION, "git", return_value="185"): + self.assertEqual( + "0.0.post185+g4ee4c6a", + VERSION.fallback_version(temp_dir, "4ee4c6a"), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/.azure-pipelines/docker-sai-test-vpp/test_evaluate_ci_baseline.py b/.azure-pipelines/docker-sai-test-vpp/test_evaluate_ci_baseline.py new file mode 100644 index 0000000000..981abeea32 --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/test_evaluate_ci_baseline.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 + +import importlib.util +import os +import tempfile +import unittest + + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +MODULE_PATH = os.path.join(SCRIPT_DIR, "evaluate_ci_baseline.py") +SPEC = importlib.util.spec_from_file_location("evaluate_ci_baseline", MODULE_PATH) +BASELINE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(BASELINE) + + +class EvaluateCiBaselineTest(unittest.TestCase): + def setUp(self): + self.temp_dir = tempfile.TemporaryDirectory() + self.addCleanup(self.temp_dir.cleanup) + self.xml_dir = os.path.join(self.temp_dir.name, "xml") + os.makedirs(self.xml_dir) + + def write_result(self, selector, result="PASS"): + result_xml = { + "PASS": "", + "FAIL": '', + "ERROR": '', + "SKIP": '', + }[result] + xml = ( + '' + f'' + f"{result_xml}" + ) + filename = f"TEST-{selector}.xml" + with open(os.path.join(self.xml_dir, filename), "w", encoding="utf-8") as xml_file: + xml_file.write(xml) + + def write_selectors(self, filename, selectors): + path = os.path.join(self.temp_dir.name, filename) + with open(path, "w", encoding="utf-8") as selector_file: + selector_file.write("\n".join(selectors) + "\n") + return path + + def test_stable_pass_with_known_failure_and_candidate(self): + self.write_result("module.StableTest") + self.write_result("module.KnownFailure", "FAIL") + self.write_result("module.NewPass") + + results = BASELINE.collect_results(self.xml_dir) + report, regressions = BASELINE.format_report( + {"module.StableTest"}, results, matrix_rc=1 + ) + + self.assertEqual([], regressions) + self.assertIn("New pass candidates: 1", report) + self.assertIn("Known non-baseline non-passes: 1", report) + + def test_regressed_and_missing_baseline_tests_fail(self): + self.write_result("module.RegressedTest", "ERROR") + + results = BASELINE.collect_results(self.xml_dir) + _, regressions = BASELINE.format_report( + {"module.RegressedTest", "module.MissingTest"}, results, matrix_rc=1 + ) + + self.assertEqual( + [("module.MissingTest", "MISSING"), ("module.RegressedTest", "ERROR")], + regressions, + ) + + def test_malformed_junit_is_infrastructure_error(self): + path = os.path.join(self.xml_dir, "TEST-broken.xml") + with open(path, "w", encoding="utf-8") as xml_file: + xml_file.write("") + + with self.assertRaisesRegex(ValueError, "cannot parse"): + BASELINE.collect_results(self.xml_dir) + + def test_duplicate_baseline_selector_is_rejected(self): + path = self.write_selectors("baseline.txt", ["module.Test", "module.Test"]) + + with self.assertRaisesRegex(ValueError, "duplicate baseline selector"): + BASELINE.read_baseline(path) + + def test_incomplete_matrix_is_rejected(self): + self.write_result("module.StableTest") + results = BASELINE.collect_results(self.xml_dir) + + error = BASELINE.validate_matrix_contract( + {"module.StableTest", "module.ExpectedTest"}, results + ) + + self.assertIn("module.ExpectedTest", error) + + def test_infrastructure_matrix_status_fails_main(self): + baseline = self.write_selectors("baseline.txt", ["module.StableTest"]) + expected = self.write_selectors("expected.txt", ["module.StableTest"]) + report = os.path.join(self.temp_dir.name, "report.txt") + + rc = BASELINE.main([ + "--xml-dir", self.xml_dir, + "--baseline", baseline, + "--expected", expected, + "--matrix-rc", "2", + "--report", report, + ]) + + self.assertEqual(1, rc) + with open(report, encoding="utf-8") as report_file: + self.assertIn("Infrastructure failure", report_file.read()) + + +if __name__ == "__main__": + unittest.main() diff --git a/.azure-pipelines/docker-sai-test-vpp/vpp_startup.conf.template b/.azure-pipelines/docker-sai-test-vpp/vpp_startup.conf.template new file mode 100644 index 0000000000..354e850b13 --- /dev/null +++ b/.azure-pipelines/docker-sai-test-vpp/vpp_startup.conf.template @@ -0,0 +1,57 @@ +unix { + nodaemon + log __VPP_LOG__ + full-coredump + cli-listen /run/vpp/cli.sock + poll-sleep-usec 100 + exec __VPP_INIT_CLI__ +} + +api-trace { + on + nitems 32768 +} + +api-segment { + global-size 256M + api-size 64M +} + +socksvr { + default +} + +memory { + main-heap-size 4G +} + +l3fib { + fib-entry-pool-size 256K + load-balance-pool-size 256K + ip4-mtrie-pool-size 256K +} + +ip6 { + heap-size 128M +} + +plugins { + plugin default { disable } + plugin af_packet_plugin.so { enable } + plugin tap_plugin.so { enable } + plugin linux_cp_plugin.so { enable } + plugin linux_nl_plugin.so { enable } + plugin acl_plugin.so { enable } + plugin vxlan_plugin.so { enable } + plugin tunterm_acl_plugin.so { enable } + plugin ip_validate_plugin.so { enable } + plugin sflow_plugin.so { enable } +} + +linux-cp { + lcp-auto-subint +} + +buffers { + buffers-per-numa __BUFFERS_PER_NUMA__ +} diff --git a/.azure-pipelines/test-sai-vpp-template.yml b/.azure-pipelines/test-sai-vpp-template.yml new file mode 100644 index 0000000000..8c7a6af260 --- /dev/null +++ b/.azure-pipelines/test-sai-vpp-template.yml @@ -0,0 +1,127 @@ +parameters: +- name: timeout + type: number + default: 360 + +- name: image_artifact_name + type: string + +- name: log_artifact_name + type: string + +jobs: +- job: RunSaiVppMatrix + displayName: Run VPP SAI compatibility tests + timeoutInMinutes: ${{ parameters.timeout }} + + pool: sonictest + + steps: + - checkout: self + clean: true + displayName: Checkout sonic-sairedis + + - task: DownloadPipelineArtifact@2 + inputs: + artifact: ${{ parameters.image_artifact_name }} + path: $(Build.ArtifactStagingDirectory)/download + displayName: Download pre-stage built VPP SAI test image + + - script: | + set -u + mapfile -t stale_containers < <(sudo docker ps -aq --filter label=com.sonic.saivpp-ci=true) + if [[ "${#stale_containers[@]}" -gt 0 ]]; then + sudo docker rm -f "${stale_containers[@]}" + fi + mapfile -t stale_images < <(sudo docker image ls -q --filter label=com.sonic.saivpp-ci=true | sort -u) + if [[ "${#stale_images[@]}" -gt 0 ]]; then + sudo docker image rm -f "${stale_images[@]}" + fi + displayName: Clean stale VPP SAI test resources + + - script: | + set -euxo pipefail + + download_dir="$(Build.ArtifactStagingDirectory)/download" + results_dir="$(Build.ArtifactStagingDirectory)/sai-vpp-results" + xml_dir="$results_dir/xml" + log_dir="$results_dir/log" + image_tag="$(cat "$download_dir/image-tag.txt")" + + mkdir -p "$xml_dir" "$log_dir" + chmod 0777 "$xml_dir" "$log_dir" + sudo docker load -i "$download_dir/docker-sai-test-vpp.gz" + sudo docker image inspect "$image_tag" > "$results_dir/image-inspect.json" + + set +e + sudo docker run --rm --privileged \ + --name "saivpp-ci-$(Build.BuildId)-$(System.JobAttempt)" \ + --label com.sonic.saivpp-ci=true \ + -e PORT_COUNT=32 \ + -e ISOLATE_EACH_TEST=1 \ + -e STARTUP_TIMEOUT=180 \ + -v "$xml_dir:/test-results" \ + -v "$log_dir:/var/log" \ + "$image_tag" \ + sai_route_test sai_rif_test sai_neighbor_test sai_ecmp_test \ + 2>&1 | tee "$results_dir/run.log" + matrix_rc=${PIPESTATUS[0]} + set -e + + echo "$matrix_rc" > "$results_dir/matrix.rc" + sudo chown -R "$(id -u):$(id -g)" "$results_dir" + chmod -R a+rX "$results_dir" + echo "VPP SAI matrix exit code: $matrix_rc" + displayName: Run VPP SAI full matrix + + - script: | + set -euxo pipefail + + results_dir="$(Build.ArtifactStagingDirectory)/sai-vpp-results" + harness_dir="$(System.DefaultWorkingDirectory)/.azure-pipelines/docker-sai-test-vpp" + matrix_rc="$(cat "$results_dir/matrix.rc")" + + python3 -c 'import defusedxml' 2>/dev/null || sudo uv pip install --system defusedxml + python3 "$harness_dir/gen_compatibility_matrix.py" \ + "$results_dir/xml" "$results_dir/compatibility-matrix.md" + python3 "$harness_dir/evaluate_ci_baseline.py" \ + --xml-dir "$results_dir/xml" \ + --baseline "$harness_dir/ci-pass-tests.txt" \ + --expected "$harness_dir/ci-matrix-tests.txt" \ + --matrix-rc "$matrix_rc" \ + --report "$results_dir/baseline-report.txt" + displayName: Evaluate VPP SAI stable baseline + + - task: PublishTestResults@2 + inputs: + testResultsFormat: JUnit + searchFolder: $(Build.ArtifactStagingDirectory)/sai-vpp-results/xml + testResultsFiles: 'TEST-*.xml' + testRunTitle: VPP SAI PTF + failTaskOnFailedTests: false + failTaskOnMissingResultsFile: true + condition: always() + + - script: | + set -euxo pipefail + cp -v "$(Build.ArtifactStagingDirectory)/download/provenance.txt" \ + "$(Build.ArtifactStagingDirectory)/sai-vpp-results/" + cp -v "$(Build.ArtifactStagingDirectory)/download/SHA256SUMS" \ + "$(Build.ArtifactStagingDirectory)/sai-vpp-results/" + displayName: Collect VPP SAI provenance + condition: always() + + - publish: $(Build.ArtifactStagingDirectory)/sai-vpp-results + artifact: ${{ parameters.log_artifact_name }}@$(System.JobAttempt) + displayName: Publish VPP SAI logs and results + condition: always() + + - script: | + set -u + tag_file="$(Build.ArtifactStagingDirectory)/download/image-tag.txt" + if [[ -f "$tag_file" ]]; then + image_tag="$(cat "$tag_file")" + sudo docker image rm "$image_tag" || true + fi + displayName: Clean VPP SAI test image + condition: always() diff --git a/.gitignore b/.gitignore index db4a306321..ea79bc7d57 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ # Packaging Files # ################### +/*.buildinfo +/*.changes +/*.deb debian/*.debhelper.log debian/*.substvars debian/.debhelper/ @@ -18,6 +21,7 @@ debian/autoreconf.after debian/autoreconf.before debian/debhelper-build-stamp debian/files +.azure-pipelines/docker-sai-test-vpp/debs/ debian/python-pysairedis/ debian/python3-pysairedis/ diff --git a/SAI b/SAI index c67f115230..a103c5d276 160000 --- a/SAI +++ b/SAI @@ -1 +1 @@ -Subproject commit c67f1152309ca08a94de0be8634a733c5cb25c35 +Subproject commit a103c5d2765edabdea896f53f295c450e5056923 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2f903223fd..2312b50b30 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -38,6 +38,9 @@ parameters: - name: debian_version type: string default: bookworm + - name: vpp_run_id + type: string + default: '' variables: - name: BUILD_BRANCH ${{ if eq(variables['Build.Reason'], 'PullRequest') }}: @@ -76,8 +79,7 @@ stages: debian_version: ${{ parameters.debian_version }} - stage: BuildArm - dependsOn: Build - condition: succeeded('Build') + dependsOn: [] jobs: - template: .azure-pipelines/build-template.yml parameters: @@ -100,17 +102,57 @@ stages: debian_version: ${{ parameters.debian_version }} - stage: BuildTrixie - dependsOn: BuildArm - condition: succeeded('BuildArm') + dependsOn: [] jobs: + - job: ResolveVpp + displayName: Resolve VPP build + pool: + vmImage: 'ubuntu-22.04' + steps: + - ${{ if eq(parameters.vpp_run_id, '') }}: + - task: DownloadPipelineArtifact@2 + name: downloadVpp + inputs: + source: specific + project: build + pipeline: sonic-net.sonic-platform-vpp + artifact: vpp-trixie + path: $(Build.ArtifactStagingDirectory)/vpp-resolution + patterns: '**/libvppinfra_*_amd64.deb' + runVersion: latestFromBranch + runBranch: refs/heads/master + allowPartiallySucceededBuilds: true + displayName: Resolve latest successful sonic platform-vpp build + - ${{ if eq(parameters.vpp_run_id, '') }}: + - script: | + set -euo pipefail + vpp_run_id="$(downloadVpp.BuildNumber)" + test -n "$vpp_run_id" + echo "Resolved sonic platform-vpp run ID: $vpp_run_id" + echo "##vso[task.setvariable variable=VPP_RUN_ID;isOutput=true]$vpp_run_id" + name: resolveVppRun + displayName: Publish resolved VPP run ID + - ${{ if ne(parameters.vpp_run_id, '') }}: + - script: | + set -euo pipefail + vpp_run_id="${{ parameters.vpp_run_id }}" + test -n "$vpp_run_id" + echo "Using requested sonic platform-vpp run ID: $vpp_run_id" + echo "##vso[task.setvariable variable=VPP_RUN_ID;isOutput=true]$vpp_run_id" + name: resolveVppRun + displayName: Publish requested VPP run ID + - template: .azure-pipelines/build-template.yml parameters: arch: amd64 + depends_on: ResolveVpp swss_common_artifact_name: sonic-swss-common-trixie artifact_name: sonic-sairedis-trixie syslog_artifact_name: sonic-sairedis-trixie.syslog run_unit_test: true + saithrift_v2: true debian_version: trixie + vpp_run_id: $(VPP_RUN_ID) - template: .azure-pipelines/build-template.yml parameters: @@ -132,6 +174,29 @@ stages: syslog_artifact_name: sonic-sairedis-trixie.syslog.arm64 debian_version: trixie +- stage: BuildSaiTestVpp + dependsOn: BuildTrixie + condition: succeeded('BuildTrixie') + variables: + VPP_RUN_ID: $[ stageDependencies.BuildTrixie.ResolveVpp.outputs['resolveVppRun.VPP_RUN_ID'] ] + jobs: + - template: .azure-pipelines/build-docker-sai-test-vpp-template.yml + parameters: + timeout: 90 + sairedis_artifact_name: sonic-sairedis-trixie + swss_common_artifact_name: sonic-swss-common-trixie + artifact_name: docker-sai-test-vpp + vpp_run_id: $(VPP_RUN_ID) + +- stage: TestSaiVpp + dependsOn: BuildSaiTestVpp + condition: succeeded('BuildSaiTestVpp') + jobs: + - template: .azure-pipelines/test-sai-vpp-template.yml + parameters: + image_artifact_name: docker-sai-test-vpp + log_artifact_name: sai-vpp-test-results + - stage: BuildSwss dependsOn: Build condition: succeeded('Build') diff --git a/pyext/py3/Makefile.am b/pyext/py3/Makefile.am index 9259566860..86090f9181 100644 --- a/pyext/py3/Makefile.am +++ b/pyext/py3/Makefile.am @@ -10,7 +10,8 @@ BUILT_SOURCES = pysairedis_wrap.cpp _pysairedis_la_SOURCES = pysairedis_wrap.cpp $(SOURCES) _pysairedis_la_CXXFLAGS = $(PYTHON3_CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS_COMMON) \ - -Wno-cast-align -Wno-cast-qual -Wno-shadow -Wno-redundant-decls + -Wno-cast-align -Wno-cast-qual -Wno-shadow -Wno-redundant-decls \ + -Wno-error=disabled-optimization _pysairedis_la_LDFLAGS = -module \ -lhiredis -lswsscommon -lpthread \ diff --git a/syncd/SwitchNotifications.h b/syncd/SwitchNotifications.h index a63a723ce5..7ad89395d3 100644 --- a/syncd/SwitchNotifications.h +++ b/syncd/SwitchNotifications.h @@ -169,6 +169,7 @@ namespace syncd .on_ipsec_post_status = &Slot::onIpsecPostStatus, .on_switch_macsec_post_status = &Slot::onSwitchMacsecPostStatus, .on_switch_ipsec_post_status = &Slot::onSwitchIpsecPostStatus, + .on_next_hop_group_hw_protection_switchover = nullptr, .on_ha_set_event = &Slot::onHaSetEvent, .on_ha_scope_event = &Slot::onHaScopeEvent, .on_flow_bulk_get_session_event = &Slot::onFlowBulkGetSessionEvent, diff --git a/unittest/meta/TestSaiSerialize.cpp b/unittest/meta/TestSaiSerialize.cpp index 67dd8e61f2..a79394e163 100644 --- a/unittest/meta/TestSaiSerialize.cpp +++ b/unittest/meta/TestSaiSerialize.cpp @@ -1299,8 +1299,8 @@ TEST(SaiSerialize, serialize_qos_map) attr.id = SAI_QOS_MAP_ATTR_MAP_TO_VALUE_LIST; sai_qos_map_t qm = { - .key = { .tc = 1, .dscp = 2, .dot1p = 3, .prio = 4, .pg = 5, .queue_index = 6, .color = SAI_PACKET_COLOR_RED, .mpls_exp = 0, .fc = 2 }, - .value = { .tc = 11, .dscp = 22, .dot1p = 33, .prio = 44, .pg = 55, .queue_index = 66, .color = SAI_PACKET_COLOR_GREEN, .mpls_exp = 0, .fc = 2 } }; + .key = { .tc = 1, .dscp = 2, .dot1p = 3, .prio = 4, .pg = 5, .queue_index = 6, .color = SAI_PACKET_COLOR_RED, .mpls_exp = 0, .fc = 2, .dei = 0, .vc = 0 }, + .value = { .tc = 11, .dscp = 22, .dot1p = 33, .prio = 44, .pg = 55, .queue_index = 66, .color = SAI_PACKET_COLOR_GREEN, .mpls_exp = 0, .fc = 2, .dei = 0, .vc = 0 } }; attr.value.qosmap.count = 1; attr.value.qosmap.list = &qm; diff --git a/unittest/syncd/TestAttrVersionChecker.cpp b/unittest/syncd/TestAttrVersionChecker.cpp index f1bc91ab94..493b6566de 100644 --- a/unittest/syncd/TestAttrVersionChecker.cpp +++ b/unittest/syncd/TestAttrVersionChecker.cpp @@ -93,6 +93,7 @@ TEST(AttrVersionChecker, reset) .iscustom = false,\ .apiversion = (v),\ .nextrelease = (n),\ + .valueprecision = 0,\ };\ diff --git a/unittest/vslib/Makefile.am b/unittest/vslib/Makefile.am index 54b8b906cd..30f5149c3e 100644 --- a/unittest/vslib/Makefile.am +++ b/unittest/vslib/Makefile.am @@ -18,6 +18,7 @@ tests_SOURCES = main.cpp \ TestLaneMap.cpp \ TestLaneMapContainer.cpp \ TestLaneMapFileParser.cpp \ + TestPortConfigFileParser.cpp \ TestMACsecAttr.cpp \ TestMACsecEgressFilter.cpp \ TestMACsecForwarder.cpp \ diff --git a/unittest/vslib/TestPortConfigFileParser.cpp b/unittest/vslib/TestPortConfigFileParser.cpp new file mode 100644 index 0000000000..30738d040c --- /dev/null +++ b/unittest/vslib/TestPortConfigFileParser.cpp @@ -0,0 +1,87 @@ +#include "PortConfigFileParser.h" + +#include "swss/logger.h" + +#include + +#include +#include +#include +#include + +using namespace saivs; + +namespace +{ + class PortConfigFileParserTest : public ::testing::Test + { + protected: + void SetUp() override + { + file = "/tmp/saivs-port-config-test.ini"; + } + + void TearDown() override + { + std::remove(file.c_str()); + } + + void write(const std::string& content) + { + SWSS_LOG_ENTER(); + + std::ofstream output(file); + ASSERT_TRUE(output.is_open()); + output << content; + } + + std::string file; + }; +} + +TEST_F(PortConfigFileParserTest, MatchesCompleteLaneSetIndependentOfOrder) +{ + write("# name lanes alias index speed\n" + "Ethernet0 25,26,27,28 fortyGigE0/0 0 40000\n"); + + auto port_config = PortConfigFileParser::parse(file); + + EXPECT_EQ("Ethernet0", port_config->getPortName({28, 26, 25, 27})); + EXPECT_EQ("", port_config->getPortName({25, 26, 27})); +} + +TEST_F(PortConfigFileParserTest, IgnoresMalformedRowsAndRejectsDuplicates) +{ + write("Ethernet0 1,2,3,4 alias 0 40000\n" + "Ethernet1 bad,lanes alias 1 40000\n" + "Ethernet2 1,2,3,4 alias 2 40000\n"); + + auto port_config = PortConfigFileParser::parse(file); + + EXPECT_EQ(1U, port_config->size()); + EXPECT_EQ("Ethernet0", port_config->getPortName({1, 2, 3, 4})); +} + +TEST(PortConfigFileParser, MissingFileReturnsEmptyMap) +{ + auto port_config = PortConfigFileParser::parse( + "/tmp/saivs-port-config-file-does-not-exist.ini"); + + EXPECT_EQ(0U, port_config->size()); +} + +TEST(PortConfigFileParser, ProductStyleNameResolvesFromLanes) +{ + const std::string file = "/tmp/saivs-product-port-config.ini"; + { + std::ofstream output(file); + ASSERT_TRUE(output.is_open()); + output << "# name lanes alias index speed\n" + "Ethernet0 25,26,27,28 fortyGigE0/0 0 40000\n"; + } + + auto port_config = PortConfigFileParser::parse(file); + + EXPECT_EQ("Ethernet0", port_config->getPortName({25, 26, 27, 28})); + std::remove(file.c_str()); +} \ No newline at end of file diff --git a/vslib/Makefile.am b/vslib/Makefile.am index f7f2f60587..445feba5a6 100644 --- a/vslib/Makefile.am +++ b/vslib/Makefile.am @@ -33,6 +33,8 @@ libSaiVS_a_SOURCES = \ LaneMapContainer.cpp \ LaneMap.cpp \ LaneMapFileParser.cpp \ + PortConfigMap.cpp \ + PortConfigFileParser.cpp \ MACsecAttr.cpp \ MACsecFilterStateGuard.cpp \ MACsecEgressFilter.cpp \ diff --git a/vslib/PortConfigFileParser.cpp b/vslib/PortConfigFileParser.cpp new file mode 100644 index 0000000000..a4b51439c3 --- /dev/null +++ b/vslib/PortConfigFileParser.cpp @@ -0,0 +1,125 @@ +#include "PortConfigFileParser.h" + +#include "swss/logger.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace saivs; + +namespace +{ + bool parseLaneSet( + _In_ const std::string& value, + _Out_ std::set& lanes) + { + SWSS_LOG_ENTER(); + + std::istringstream lane_stream(value); + std::string lane_token; + size_t lane_count = 0; + + while (std::getline(lane_stream, lane_token, ',')) + { + if (lane_token.empty()) + { + return false; + } + + size_t parsed = 0; + unsigned long lane; + try + { + lane = std::stoul(lane_token, &parsed, 10); + } + catch (const std::exception&) + { + return false; + } + + if (parsed != lane_token.size() || + lane > std::numeric_limits::max()) + { + return false; + } + + lane_count++; + if (!lanes.insert(static_cast(lane)).second) + { + return false; + } + } + + return lane_count != 0 && lane_count == lanes.size(); + } +} + +std::shared_ptr PortConfigFileParser::parse( + _In_ const char* file) +{ + SWSS_LOG_ENTER(); + + if (file == nullptr) + { + SWSS_LOG_WARN("no port config file specified"); + return std::make_shared(); + } + + return parse(std::string(file)); +} + +std::shared_ptr PortConfigFileParser::parse( + _In_ const std::string& file) +{ + SWSS_LOG_ENTER(); + + auto port_config = std::make_shared(); + if (file.empty()) + { + SWSS_LOG_WARN("no port config file specified"); + return port_config; + } + + std::ifstream input(file); + if (!input.is_open()) + { + SWSS_LOG_WARN("failed to open port config file: %s: %s", + file.c_str(), strerror(errno)); + return port_config; + } + + std::string line; + size_t line_number = 0; + while (std::getline(input, line)) + { + line_number++; + std::istringstream fields(line); + std::string name; + std::string lane_value; + + if (!(fields >> name >> lane_value) || name[0] == '#' || name[0] == ';') + { + continue; + } + + std::set lanes; + if (!parseLaneSet(lane_value, lanes)) + { + SWSS_LOG_WARN("invalid port config lane list at %s:%zu", + file.c_str(), line_number); + continue; + } + + port_config->add(lanes, name); + } + + SWSS_LOG_NOTICE("loaded %zu port config entries from %s", + port_config->size(), file.c_str()); + return port_config; +} \ No newline at end of file diff --git a/vslib/PortConfigFileParser.h b/vslib/PortConfigFileParser.h new file mode 100644 index 0000000000..da68d6f5cd --- /dev/null +++ b/vslib/PortConfigFileParser.h @@ -0,0 +1,25 @@ +#pragma once + +#include "PortConfigMap.h" + +#include "swss/sal.h" + +#include +#include + +namespace saivs +{ + class PortConfigFileParser + { + private: + PortConfigFileParser() = delete; + ~PortConfigFileParser() = delete; + + public: + static std::shared_ptr parse( + _In_ const char* file); + + static std::shared_ptr parse( + _In_ const std::string& file); + }; +} \ No newline at end of file diff --git a/vslib/PortConfigMap.cpp b/vslib/PortConfigMap.cpp new file mode 100644 index 0000000000..db6248008e --- /dev/null +++ b/vslib/PortConfigMap.cpp @@ -0,0 +1,49 @@ +#include "PortConfigMap.h" + +#include "swss/logger.h" + +using namespace saivs; + +bool PortConfigMap::add( + _In_ const std::set& lanes, + _In_ const std::string& name) +{ + SWSS_LOG_ENTER(); + + if (lanes.empty() || name.empty()) + { + SWSS_LOG_WARN("cannot add empty port config entry for %s", name.c_str()); + return false; + } + + auto result = m_lanes_to_name.emplace(lanes, name); + if (!result.second) + { + SWSS_LOG_WARN("duplicate port config lane set for %s and %s", + result.first->second.c_str(), name.c_str()); + return false; + } + + return true; +} + +std::string PortConfigMap::getPortName( + _In_ const std::set& lanes) const +{ + SWSS_LOG_ENTER(); + + auto it = m_lanes_to_name.find(lanes); + if (it == m_lanes_to_name.end()) + { + return ""; + } + + return it->second; +} + +size_t PortConfigMap::size() const +{ + SWSS_LOG_ENTER(); + + return m_lanes_to_name.size(); +} \ No newline at end of file diff --git a/vslib/PortConfigMap.h b/vslib/PortConfigMap.h new file mode 100644 index 0000000000..d2d1ef3036 --- /dev/null +++ b/vslib/PortConfigMap.h @@ -0,0 +1,28 @@ +#pragma once + +#include "swss/sal.h" + +#include +#include +#include +#include +#include + +namespace saivs +{ + class PortConfigMap + { + public: + bool add( + _In_ const std::set& lanes, + _In_ const std::string& name); + + std::string getPortName( + _In_ const std::set& lanes) const; + + size_t size() const; + + private: + std::map, std::string> m_lanes_to_name; + }; +} \ No newline at end of file diff --git a/vslib/saivs.h b/vslib/saivs.h index ea5fc3c28e..c5ab0c45a4 100644 --- a/vslib/saivs.h +++ b/vslib/saivs.h @@ -23,6 +23,15 @@ extern "C" { */ #define SAI_KEY_VS_INTERFACE_LANE_MAP_FILE "SAI_VS_INTERFACE_LANE_MAP_FILE" +/** + * @def SAI_KEY_VS_PORT_CONFIG_FILE + * + * Optional path to the SONiC port configuration used to resolve a port lane + * set to its SONiC interface name. The default port configuration is used when + * this setting is omitted. + */ +#define SAI_KEY_VS_PORT_CONFIG_FILE "SAI_VS_PORT_CONFIG_FILE" + /** * @def SAI_KEY_VS_RESOURCE_LIMITER_FILE * diff --git a/vslib/vpp/SwitchVpp.cpp b/vslib/vpp/SwitchVpp.cpp index b17a84e1e6..daf9825b28 100644 --- a/vslib/vpp/SwitchVpp.cpp +++ b/vslib/vpp/SwitchVpp.cpp @@ -10,7 +10,9 @@ #include "vppxlate/SaiIntfStats.h" #include "vppxlate/SaiRouteStats.h" +#include "PortConfigFileParser.h" #include "SwitchVppUtils.h" +#include "saivs.h" #include #include @@ -21,6 +23,9 @@ using namespace saivs; namespace { + constexpr const char *DEFAULT_PORT_CONFIG_FILE = + "/usr/share/sonic/hwsku/port_config.ini"; + constexpr uint64_t ROUTE_COUNTER_RESET_DELTA_THRESHOLD = 1ULL << 60; // TTL for the route-stats full-dump cache. Must be shorter than the @@ -54,6 +59,8 @@ SwitchVpp::SwitchVpp( { SWSS_LOG_ENTER(); + loadPortConfig(); + vpp_dp_initialize(); } @@ -70,6 +77,8 @@ SwitchVpp::SwitchVpp( { SWSS_LOG_ENTER(); + loadPortConfig(); + vpp_dp_initialize(); } @@ -92,6 +101,19 @@ SwitchVpp::~SwitchVpp() SWSS_LOG_NOTICE("SwitchVpp destructor completed"); } +void SwitchVpp::loadPortConfig() +{ + SWSS_LOG_ENTER(); + + const auto &profileMap = m_switchConfig->m_profileMap; + const auto portConfigFile = profileMap.find(SAI_KEY_VS_PORT_CONFIG_FILE); + const std::string portConfigPath = portConfigFile == profileMap.end() + ? DEFAULT_PORT_CONFIG_FILE + : portConfigFile->second; + + m_portConfigMap = PortConfigFileParser::parse(portConfigPath); +} + void SwitchVpp::deinitFdbEventHandling() { SWSS_LOG_ENTER(); @@ -1673,6 +1695,49 @@ sai_status_t SwitchVpp::create_internal( return SAI_STATUS_SUCCESS; } +sai_status_t SwitchVpp::create_port_dependencies( + _In_ sai_object_id_t port_id, + _In_ uint32_t attr_count, + _In_ const sai_attribute_t *attr_list) +{ + SWSS_LOG_ENTER(); + + SWSS_LOG_WARN("check attributes and set, FIXME"); + + sai_attribute_t attr; + + if (sai_metadata_get_attr_by_id(SAI_PORT_ATTR_ADMIN_STATE, attr_count, attr_list) == nullptr) + { + attr.id = SAI_PORT_ATTR_ADMIN_STATE; + attr.value.booldata = false; + + CHECK_STATUS(set(SAI_OBJECT_TYPE_PORT, port_id, &attr)); + } + + if (sai_metadata_get_attr_by_id(SAI_PORT_ATTR_HOST_TX_READY_STATUS, attr_count, attr_list) == nullptr) + { + attr.id = SAI_PORT_ATTR_HOST_TX_READY_STATUS; + attr.value.u32 = SAI_PORT_HOST_TX_READY_STATUS_READY; + + CHECK_STATUS(set(SAI_OBJECT_TYPE_PORT, port_id, &attr)); + } + + if (sai_metadata_get_attr_by_id(SAI_PORT_ATTR_AUTO_NEG_MODE, attr_count, attr_list) == nullptr) + { + attr.id = SAI_PORT_ATTR_AUTO_NEG_MODE; + attr.value.booldata = true; + + CHECK_STATUS(set(SAI_OBJECT_TYPE_PORT, port_id, &attr)); + } + + CHECK_STATUS(create_ingress_priority_groups_per_port(port_id)); + CHECK_STATUS(create_qos_queues_per_port(port_id)); + CHECK_STATUS(create_scheduler_groups_per_port(port_id)); + CHECK_STATUS(create_port_serdes_per_port(port_id)); + + return SAI_STATUS_SUCCESS; +} + sai_status_t SwitchVpp::createPort( _In_ sai_object_id_t object_id, _In_ sai_object_id_t switch_id, @@ -1681,7 +1746,7 @@ sai_status_t SwitchVpp::createPort( { SWSS_LOG_ENTER(); - UpdatePort(object_id, attr_count, attr_list); + CHECK_STATUS(UpdatePort(object_id, attr_count, attr_list)); auto sid = sai_serialize_object_id(object_id); @@ -1706,7 +1771,7 @@ sai_status_t SwitchVpp::createPort( CHECK_STATUS(create_internal(SAI_OBJECT_TYPE_PORT, sid, switch_id, attr_count, attr_list)); } - return create_port_dependencies(object_id); + return create_port_dependencies(object_id, attr_count, attr_list); } diff --git a/vslib/vpp/SwitchVpp.h b/vslib/vpp/SwitchVpp.h index cedd0ae0e2..4c05866c38 100644 --- a/vslib/vpp/SwitchVpp.h +++ b/vslib/vpp/SwitchVpp.h @@ -9,6 +9,7 @@ #include "SwitchVppNexthop.h" #include "SwitchVppAcl.h" #include "CRMTracker.h" +#include "PortConfigMap.h" #include "vppxlate/SaiVppXlate.h" #include "vppxlate/SaiRouteStats.h" @@ -275,6 +276,11 @@ namespace saivs _In_ uint32_t attr_count, _In_ const sai_attribute_t *attr_list) override; + sai_status_t create_port_dependencies( + _In_ sai_object_id_t port_id, + _In_ uint32_t attr_count, + _In_ const sai_attribute_t *attr_list); + virtual sai_status_t setPort( _In_ sai_object_id_t portId, _In_ const sai_attribute_t* attr) override; @@ -700,12 +706,16 @@ namespace saivs sai_status_t vpp_set_interface_state ( _In_ sai_object_id_t object_id, _In_ uint32_t vlan_id, - _In_ bool is_up); + _In_ bool is_up, + _In_ uint32_t attr_count = 0, + _In_ const sai_attribute_t *attr_list = nullptr); // set ethernet interface mtu including L2 header sai_status_t vpp_set_port_mtu ( _In_ sai_object_id_t object_id, _In_ uint32_t vlan_id, - _In_ uint32_t mtu); + _In_ uint32_t mtu, + _In_ uint32_t attr_count = 0, + _In_ const sai_attribute_t *attr_list = nullptr); // set sw interface mtu excluding L2 header sai_status_t vpp_set_interface_mtu ( _In_ sai_object_id_t object_id, @@ -716,7 +726,9 @@ namespace saivs sai_status_t vpp_set_port_speed ( _In_ sai_object_id_t object_id, _In_ uint32_t vlan_id, - _In_ uint32_t speed); + _In_ uint32_t speed, + _In_ uint32_t attr_count = 0, + _In_ const sai_attribute_t *attr_list = nullptr); sai_status_t UpdatePort( _In_ sai_object_id_t object_id, @@ -1139,7 +1151,9 @@ namespace saivs bool vpp_get_hwif_name ( _In_ sai_object_id_t object_id, _In_ uint32_t vlan_id, - _Out_ std::string& ifname); + _Out_ std::string& ifname, + _In_ uint32_t attr_count = 0, + _In_ const sai_attribute_t *attr_list = nullptr); public: @@ -1160,6 +1174,15 @@ namespace saivs void populate_if_mapping(); + bool getPortHwifNameFromLane( + _In_ sai_object_id_t port_id, + _Out_ std::string& if_name); + + bool getPortHwifNameFromLane( + _In_ sai_object_id_t port_id, + _In_ uint32_t attr_count, + _In_ const sai_attribute_t *attr_list, + _Out_ std::string& if_name); bool getTapNameFromPortOrLagId( _In_ sai_object_id_t obj_id, _Out_ std::string& if_name); @@ -1188,6 +1211,9 @@ namespace saivs void startVppEventsThread(); private: // VPP + void loadPortConfig(); + + std::shared_ptr m_portConfigMap; std::map m_hostif_hwif_map; std::map m_hwif_hostif_map; diff --git a/vslib/vpp/SwitchVppFdb.cpp b/vslib/vpp/SwitchVppFdb.cpp index 95c34bb9d0..9a8e35ef2d 100644 --- a/vslib/vpp/SwitchVppFdb.cpp +++ b/vslib/vpp/SwitchVppFdb.cpp @@ -1102,10 +1102,11 @@ sai_status_t SwitchVpp::vpp_create_lag( mode = VPP_BOND_API_MODE_XOR; lb = VPP_BOND_API_LB_ALGO_L34_INNER; - create_bond_interface(bond_id, mode, lb, &swif_idx); - if (swif_idx == static_cast(~0)) + int ret = create_bond_interface(bond_id, mode, lb, &swif_idx); + if (ret != 0 || swif_idx == static_cast(~0) || swif_idx == 0) { - SWSS_LOG_ERROR("failed to create bond interface in VPP for %s", sai_serialize_object_id(lag_id).c_str()); + SWSS_LOG_ERROR("failed to create bond interface in VPP for %s (ret=%d, swif_idx=%u)", + sai_serialize_object_id(lag_id).c_str(), ret, swif_idx); return SAI_STATUS_FAILURE; } diff --git a/vslib/vpp/SwitchVppRif.cpp b/vslib/vpp/SwitchVppRif.cpp index 454d557c5a..b6300dc9ac 100644 --- a/vslib/vpp/SwitchVppRif.cpp +++ b/vslib/vpp/SwitchVppRif.cpp @@ -25,6 +25,8 @@ #include #include #include +#include +#include using namespace saivs; @@ -339,39 +341,162 @@ void SwitchVpp::vpp_intf_remove_prefix_entry (const std::string& intf_name) m_intf_prefix_map.erase(it); } -bool SwitchVpp::vpp_get_hwif_name ( - _In_ sai_object_id_t object_id, - _In_ uint32_t vlan_id, - _Out_ std::string& ifname) +bool SwitchVpp::getPortHwifNameFromLane( + _In_ sai_object_id_t port_id, + _Out_ std::string& if_name) { SWSS_LOG_ENTER(); - std::string tap_name; + return getPortHwifNameFromLane(port_id, 0, nullptr, if_name); +} - if (getTapNameFromPortOrLagId(object_id, tap_name) == false) +bool SwitchVpp::getPortHwifNameFromLane( + _In_ sai_object_id_t port_id, + _In_ uint32_t attr_count, + _In_ const sai_attribute_t *attr_list, + _Out_ std::string& if_name) +{ + SWSS_LOG_ENTER(); + + if (!m_portConfigMap) { - SWSS_LOG_ERROR("host interface for port/lag id %s not found", - sai_serialize_object_id(object_id).c_str()); + SWSS_LOG_ERROR("port config map unavailable for port %s", + sai_serialize_object_id(port_id).c_str()); return false; } - const char *hwifname = tap_to_hwif_name(tap_name.c_str()); + uint32_t lanes[8] = {}; + uint32_t lane_count = 0; + const sai_attribute_t *lane_attr = nullptr; + if (attr_list != nullptr) + { + lane_attr = sai_metadata_get_attr_by_id( + SAI_PORT_ATTR_HW_LANE_LIST, attr_count, attr_list); + } - if (hwifname == NULL || strcmp(hwifname, "Unknown") == 0) + if (lane_attr != nullptr) + { + lane_count = lane_attr->value.u32list.count; + if (lane_count > sizeof(lanes) / sizeof(lanes[0])) + { + SWSS_LOG_ERROR("too many lanes for port %s", + sai_serialize_object_id(port_id).c_str()); + return false; + } + if (lane_count != 0 && lane_attr->value.u32list.list == nullptr) + { + SWSS_LOG_ERROR("port %s has a null lane list", + sai_serialize_object_id(port_id).c_str()); + return false; + } + if (lane_count != 0) + { + std::copy(lane_attr->value.u32list.list, + lane_attr->value.u32list.list + lane_count, lanes); + } + } + else { + sai_attribute_t attr = {}; + attr.id = SAI_PORT_ATTR_HW_LANE_LIST; + attr.value.u32list.count = sizeof(lanes) / sizeof(lanes[0]); + attr.value.u32list.list = lanes; + + if (get(SAI_OBJECT_TYPE_PORT, port_id, 1, &attr) != SAI_STATUS_SUCCESS) + { + SWSS_LOG_ERROR("lane list unavailable for port %s", + sai_serialize_object_id(port_id).c_str()); + return false; + } + lane_count = attr.value.u32list.count; + } + + if (lane_count == 0 || lane_count > sizeof(lanes) / sizeof(lanes[0])) + { + SWSS_LOG_ERROR("lane list unavailable for port %s", + sai_serialize_object_id(port_id).c_str()); return false; } - char hw_subifname[64]; - const char *hw_ifname; + std::set lane_set(lanes, lanes + lane_count); + if (lane_set.size() != lane_count) + { + SWSS_LOG_ERROR("duplicate lanes in port %s lane list", + sai_serialize_object_id(port_id).c_str()); + return false; + } + const std::string port_name = + m_portConfigMap->getPortName(lane_set); + if (port_name.empty()) + { + SWSS_LOG_ERROR("lane set does not map to a port for %s", + sai_serialize_object_id(port_id).c_str()); + return false; + } + + const char *mapped_hwifname = tap_to_hwif_name(port_name.c_str()); + if (mapped_hwifname == nullptr || strcmp(mapped_hwifname, "Unknown") == 0) + { + SWSS_LOG_ERROR("port %s has no VPP mapping", port_name.c_str()); + return false; + } + + if (vpp_get_swif_idx_by_name(mapped_hwifname) == static_cast(-1)) + { + SWSS_LOG_ERROR("VPP interface %s is not present for port %s", + mapped_hwifname, port_name.c_str()); + return false; + } + + if_name = mapped_hwifname; + SWSS_LOG_INFO("resolved port %s lane set to %s/%s", + sai_serialize_object_id(port_id).c_str(), port_name.c_str(), + if_name.c_str()); + return true; +} + +bool SwitchVpp::vpp_get_hwif_name ( + _In_ sai_object_id_t object_id, + _In_ uint32_t vlan_id, + _Out_ std::string& ifname, + _In_ uint32_t attr_count, + _In_ const sai_attribute_t *attr_list) +{ + SWSS_LOG_ENTER(); + + std::string hwifname; + if (objectTypeQuery(object_id) == SAI_OBJECT_TYPE_PORT && + getPortHwifNameFromLane(object_id, attr_count, attr_list, hwifname)) + { + SWSS_LOG_DEBUG("using lane-based VPP interface %s for port %s", + hwifname.c_str(), sai_serialize_object_id(object_id).c_str()); + } + else + { + std::string tap_name; + + if (getTapNameFromPortOrLagId(object_id, tap_name) == false) + { + SWSS_LOG_ERROR("host interface for port/lag id %s not found", + sai_serialize_object_id(object_id).c_str()); + return false; + } + + const char *mapped_hwifname = tap_to_hwif_name(tap_name.c_str()); + + if (mapped_hwifname == NULL || strcmp(mapped_hwifname, "Unknown") == 0) + { + return false; + } + + hwifname = mapped_hwifname; + } if (vlan_id) { - snprintf(hw_subifname, sizeof(hw_subifname), "%s.%u", hwifname, vlan_id); - hw_ifname = hw_subifname; + ifname = hwifname + "." + std::to_string(vlan_id); } else { - hw_ifname = hwifname; + ifname = hwifname; } - ifname = std::string(hw_ifname); return true; } @@ -585,7 +710,9 @@ sai_status_t SwitchVpp::asyncIntfStateUpdate(const char *hwif_name, bool link_up sai_status_t SwitchVpp::vpp_set_interface_state ( _In_ sai_object_id_t object_id, _In_ uint32_t vlan_id, - _In_ bool is_up) + _In_ bool is_up, + _In_ uint32_t attr_count, + _In_ const sai_attribute_t *attr_list) { SWSS_LOG_ENTER(); @@ -595,7 +722,8 @@ sai_status_t SwitchVpp::vpp_set_interface_state ( std::string ifname; - if (vpp_get_hwif_name(object_id, vlan_id, ifname) == true) { + if (vpp_get_hwif_name(object_id, vlan_id, ifname, attr_count, attr_list)) + { const char *hwif_name = ifname.c_str(); interface_set_state(hwif_name, is_up); @@ -608,7 +736,9 @@ sai_status_t SwitchVpp::vpp_set_interface_state ( sai_status_t SwitchVpp::vpp_set_port_mtu ( _In_ sai_object_id_t object_id, _In_ uint32_t vlan_id, - _In_ uint32_t mtu) + _In_ uint32_t mtu, + _In_ uint32_t attr_count, + _In_ const sai_attribute_t *attr_list) { SWSS_LOG_ENTER(); @@ -618,7 +748,8 @@ sai_status_t SwitchVpp::vpp_set_port_mtu ( std::string ifname; - if (vpp_get_hwif_name(object_id, vlan_id, ifname) == true) { + if (vpp_get_hwif_name(object_id, vlan_id, ifname, attr_count, attr_list)) + { const char *hwif_name = ifname.c_str(); hw_interface_set_mtu(hwif_name, mtu); @@ -653,7 +784,9 @@ sai_status_t SwitchVpp::vpp_set_interface_mtu ( sai_status_t SwitchVpp::vpp_set_port_speed ( _In_ sai_object_id_t object_id, _In_ uint32_t vlan_id, - _In_ uint32_t speed) + _In_ uint32_t speed, + _In_ uint32_t attr_count, + _In_ const sai_attribute_t *attr_list) { SWSS_LOG_ENTER(); @@ -663,7 +796,8 @@ sai_status_t SwitchVpp::vpp_set_port_speed ( std::string ifname; - if (vpp_get_hwif_name(object_id, vlan_id, ifname) == true) { + if (vpp_get_hwif_name(object_id, vlan_id, ifname, attr_count, attr_list)) + { const char *hwif_name = ifname.c_str(); // SAI port speed is in Mbps, VPP link speed is in Kbps @@ -755,21 +889,24 @@ sai_status_t SwitchVpp::UpdatePort( if (attr_type != NULL) { - vpp_set_interface_state(object_id, 0, attr_type->value.booldata); + vpp_set_interface_state(object_id, 0, attr_type->value.booldata, + attr_count, attr_list); } attr_type = sai_metadata_get_attr_by_id(SAI_PORT_ATTR_MTU, attr_count, attr_list); if (attr_type != NULL) { - vpp_set_port_mtu(object_id, 0, attr_type->value.u32); + vpp_set_port_mtu(object_id, 0, attr_type->value.u32, + attr_count, attr_list); } attr_type = sai_metadata_get_attr_by_id(SAI_PORT_ATTR_SPEED, attr_count, attr_list); if (attr_type != NULL) { - vpp_set_port_speed(object_id, 0, attr_type->value.u32); + vpp_set_port_speed(object_id, 0, attr_type->value.u32, + attr_count, attr_list); } return SAI_STATUS_SUCCESS; diff --git a/vslib/vpp/SwitchVppRoute.cpp b/vslib/vpp/SwitchVppRoute.cpp index a33e6eedc9..d2eae7a271 100644 --- a/vslib/vpp/SwitchVppRoute.cpp +++ b/vslib/vpp/SwitchVppRoute.cpp @@ -162,7 +162,35 @@ sai_status_t SwitchVpp::IpRouteAddRemove( packet_action = attr.value.s32; } - // We should program drop routes + sai_route_entry_t route_entry; + sai_deserialize_route_entry(serializedObjectId, route_entry); + + if (packet_action == SAI_PACKET_ACTION_DROP) { + std::shared_ptr vrf = vpp_get_ip_vrf(route_entry.vr_id); + uint32_t vrf_id = vrf == nullptr ? 0 : vrf->m_vrf_id; + vpp_ip_route_t *ip_route = (vpp_ip_route_t *) + calloc(1, sizeof(vpp_ip_route_t) + sizeof(vpp_ip_nexthop_t)); + if (!ip_route) { + return SAI_STATUS_FAILURE; + } + + create_route_prefix_entry(&route_entry, ip_route); + ip_route->vrf_id = vrf_id; + ip_route->nexthop_cnt = 1; + ip_route->nexthop[0].addr.sa_family = ip_route->prefix_addr.sa_family; + ip_route->nexthop[0].sw_if_index = (uint32_t)~0; + ip_route->nexthop[0].weight = 1; + ip_route->nexthop[0].type = VPP_NEXTHOP_DROP; + + ret = ip_route_add_del_get_stats(ip_route, is_add, is_add ? stats_index : NULL); + free(ip_route); + + SWSS_LOG_NOTICE("%s drop route in VS %s status %d table %u", + is_add ? "Add" : "Remove", + serializedObjectId.c_str(), ret, vrf_id); + return ret == 0 ? SAI_STATUS_SUCCESS : SAI_STATUS_FAILURE; + } + if (packet_action != SAI_PACKET_ACTION_FORWARD) { SWSS_LOG_NOTICE("Ignoring ip route %s: action is not forward: %d", serializedObjectId.c_str(), packet_action); @@ -178,13 +206,10 @@ sai_status_t SwitchVpp::IpRouteAddRemove( } next_hop_oid = attr.value.oid; - sai_route_entry_t route_entry; const char *hwif_name = NULL; vpp_nexthop_type_e nexthop_type = VPP_NEXTHOP_NORMAL; bool config_ip_route = false; - sai_deserialize_route_entry(serializedObjectId, route_entry); - nexthop_grp_config_t *nxthop_group = NULL; if (SAI_OBJECT_TYPE_ROUTER_INTERFACE == RealObjectIdManager::objectTypeQuery(next_hop_oid)) diff --git a/vslib/vpp/vppxlate/SaiVppXlate.c b/vslib/vpp/vppxlate/SaiVppXlate.c index e1146dff56..e4bf342c00 100644 --- a/vslib/vpp/vppxlate/SaiVppXlate.c +++ b/vslib/vpp/vppxlate/SaiVppXlate.c @@ -2969,6 +2969,8 @@ int ip_route_add_del_get_stats (vpp_ip_route_t *prefix, bool is_add, uint32_t *s fib_path->type = htonl(FIB_API_PATH_TYPE_NORMAL); } else if (nexthop->type == VPP_NEXTHOP_LOCAL) { fib_path->type = htonl(FIB_API_PATH_TYPE_LOCAL); + } else if (nexthop->type == VPP_NEXTHOP_DROP) { + fib_path->type = htonl(FIB_API_PATH_TYPE_DROP); } fib_path->table_id = 0; fib_path->rpf_id = htonl((uint32_t)~0); diff --git a/vslib/vpp/vppxlate/SaiVppXlate.h b/vslib/vpp/vppxlate/SaiVppXlate.h index c52e9a178d..f017d89f22 100644 --- a/vslib/vpp/vppxlate/SaiVppXlate.h +++ b/vslib/vpp/vppxlate/SaiVppXlate.h @@ -24,7 +24,8 @@ extern "C" { typedef enum { VPP_NEXTHOP_NORMAL = 1, - VPP_NEXTHOP_LOCAL = 2 + VPP_NEXTHOP_LOCAL = 2, + VPP_NEXTHOP_DROP = 3 } vpp_nexthop_type_e; typedef struct vpp_ip_addr_ {