Test a SONiC change in CI¶
SONiC is different from most network OSes in a lab: the virtual switch image boots with no configuration, and its services come up one by one after the container starts. This guide shows how to get the image, configure the switch once it's ready, and check its routing on every pull request. It uses examples/sonic-frr: a SONiC VS switch peering eBGP with an FRR router. Every output below is from a real run.
1. Get the image¶
docker-sonic-vs is the virtual switch the SONiC project builds for its own tests. It is not on a container registry: the SONiC build server publishes it as a compressed file. Download it and load it into Docker:
curl -L -o docker-sonic-vs.gz \
"https://sonic-build.azurewebsites.net/api/sonic/artifacts?branchName=master&platform=vs&target=target/docker-sonic-vs.gz"
docker load -i docker-sonic-vs.gz # creates docker-sonic-vs:latest
The file is about 230 MB (232,075,139 bytes for the build used on this page, downloaded on 2026-10-27). Loaded, the image is 1.92 GB.
branchName=master is the latest build, so it changes over time. For repeatable CI, keep the file you tested with (as a release asset or in a cache) instead of downloading master on every run.
2. The lab¶
name: ndr-sonic-frr
topology:
nodes:
sonic1:
kind: sonic-vs
image: docker-sonic-vs:latest
frr1:
kind: linux
image: quay.io/frrouting/frr:10.4.1
binds:
- configs/daemons:/etc/frr/daemons
- configs/vtysh.conf:/etc/frr/vtysh.conf
- configs/frr1.conf:/etc/frr/frr.conf
links:
- endpoints: ["sonic1:eth1", "frr1:eth1"]
In kind: sonic-vs, the container's eth1 is the SONiC port Ethernet0. The topology names eth1; SONiC commands and checks use Ethernet0.
3. Configure SONiC after boot: setup¶
There's no startup config. Instead, netdryrun.yml lists setup commands that Netdryrun runs inside the node after deploying the lab and before any check:
lab: topology.clab.yml
timeout: 180
nodes:
sonic1:
setup:
- supervisorctl status orchagent | grep -q RUNNING && supervisorctl status bgpd | grep -q RUNNING
- config interface ip add Ethernet0 10.0.40.1/30 && config interface startup Ethernet0
- config loopback add Loopback0; config interface ip add Loopback0 192.168.40.1/32
- >-
vtysh -c "conf t" -c "router bgp 65040" -c "bgp router-id 40.40.40.40" -c "no bgp ebgp-requires-policy"
-c "neighbor 10.0.40.2 remote-as 65020" -c "address-family ipv4 unicast" -c "network 192.168.40.1/32"
Line by line:
- Wait for the services. The container is "running" long before SONiC is.
orchagent(which programs the switch) andbgpd(FRR inside SONiC) have to beRUNNINGbeforeconfigcommands andvtyshwork. This line fails until they are. - Address and enable the port with SONiC's
configCLI, which writes to CONFIG_DB like on a real switch. - A loopback for the announced prefix.
- BGP, configured in the FRR that runs inside SONiC, through
vtysh.
Each line is retried every 5 seconds until it succeeds, within a shared budget of 300 seconds. A line runs again from the start if any part of it fails, so put the part that can fail first (as line 1 does), and keep lines that change state safe to run twice.
4. The checks that matter for SONiC¶
checks:
- type: bgp_session
node: sonic1
neighbor: 10.0.40.2
- type: route
node: sonic1
prefix: 192.168.20.0/24
protocol: bgp
- type: route
node: frr1
prefix: 192.168.40.1/32
protocol: bgp
- type: ping
node: sonic1
source: 192.168.40.1
target: 192.168.20.1
- type: bgp_prefixes
node: frr1
neighbor: 10.0.40.1
min: 1
max: 5
- type: bgp_route
node: sonic1
prefix: 192.168.20.0/24
as_path: "65020"
local_pref: 100
next_hop: 10.0.40.2
scenarios:
- name: SONiC port Ethernet0 goes down
link_down: {node: sonic1, interface: Ethernet0}
checks:
- {type: bgp_session, node: frr1, neighbor: 10.0.40.1, state: down}
- {type: route, node: frr1, prefix: 192.168.40.1/32, protocol: bgp, present: false}
What to know about these on SONiC:
- Routing state comes from the FRR inside SONiC. The
sonicdriver reads sessions, routes and BGP paths throughvtysh, exactly as on FRR. A route check says what SONiC's routing stack selected. It does not read whatorchagentprogrammed (APPL_DB, ASIC_DB). pingcovers the data path. It runs from the switch with the loopback as source, so it only passes if the port, its address and the route all work. Keep at least one ping per change.- Check from the other side too.
routeandbgp_prefixesonfrr1prove what SONiC actually announces, which is what a policy change on the switch affects. - A port-down scenario uses the SONiC name.
link_downonEthernet0takes the port down inside the switch. The scenario then checks that the peer sees the session drop and the route withdrawn, and Netdryrun brings the port back afterwards. - Give it time.
timeout: 180covers the slow boot; thesetupbudget is separate.
5. Run it locally¶
Netdryrun 0.3.0: 6 checks from examples/sonic-frr/netdryrun.yml
Deploying lab ndr-sonic-frr (2 nodes); this usually takes 30-90 s, longer the first time images are pulled...
Running setup on sonic1...
setup done on sonic1 (4 commands)
[PASS] sonic1: BGP session to 10.0.40.2 is established -- state is established (13.5s)
[PASS] sonic1: route 192.168.20.0/24 via bgp is present -- active via bgp (13.5s)
[PASS] frr1: route 192.168.40.1/32 via bgp is present -- active via bgp (13.5s)
[PASS] sonic1: ping 192.168.20.1 from 192.168.40.1 expects success -- 3/3 replies (need 2) (13.5s)
[PASS] sonic1: BGP best path for 192.168.20.0/24 has as-path '65020', local-pref 100, next hop 10.0.40.2 -- best path: as-path '65020', next hop 10.0.40.2, local-pref 100, MED 0, communities none (13.5s)
Convergence checks done; settling 10s before post-convergence checks...
[PASS] frr1: prefixes from 10.0.40.1 >= 1 and <= 5 -- 1 ipv4-unicast prefixes accepted (held 10s, 5 samples) (10.2s)
Scenario: SONiC port Ethernet0 goes down
[PASS] [SONiC port Ethernet0 goes down] frr1: BGP session to 10.0.40.1 is down -- state is connect (held 10s, 5 samples) (10.1s)
[PASS] [SONiC port Ethernet0 goes down] frr1: route 192.168.40.1/32 via bgp is absent -- absent (held 10s, 5 samples) (10.1s)
Restored 1 link(s) and 0 route injection(s); settling 10s
Destroying lab...
PASSED: 8 passed, 0 failed (8 checks)
6. Run it on every pull request¶
The workflow is the usual one with one extra step: load the image before Netdryrun deploys the lab. containerlab only pulls images that are missing, so the loaded docker-sonic-vs:latest is used as is.
name: SONiC dry run
on:
pull_request:
paths:
- "network/sonic/**"
jobs:
dryrun:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Load docker-sonic-vs
run: |
curl -fsSL -o docker-sonic-vs.gz \
"https://sonic-build.azurewebsites.net/api/sonic/artifacts?branchName=master&platform=vs&target=target/docker-sonic-vs.gz"
docker load -i docker-sonic-vs.gz
- uses: netdryrun/netdryrun@v0.3.0
with:
path: network/sonic/
On other CI systems: load the image the same way, then install containerlab and Netdryrun and run sudo env "PATH=$PATH" netdryrun run <folder>, as in the FRR guide.
This workflow is a recipe, not something Netdryrun's own CI runs yet: SONiC isn't in the project's CI, and the workflow hasn't been run on a hosted runner. The runs on this page are local.
Times¶
Wall-clock for netdryrun run, including deploying the lab, the setup lines, the checks, the port-down scenario and destroying the lab, with both images already loaded. Measured on 2026-10-27 on WSL2 (20 cores, 16 GB), containerlab 0.81.1, Netdryrun 0.3.0, from a copy of the example folder with its own lab name:
| Run | Result | Exit | Time |
|---|---|---|---|
examples/sonic-frr (6 checks and a port-down scenario) |
PASSED: 8 passed, 0 failed |
0 |
97 s |
The checks passed 13.5 s after setup finished. The examples page shows 90 s from an earlier run on the same machine and date; runs differ by a few seconds. In CI, add the time to download the 230 MB image, which depends on the runner's network; caching the file avoids it.
Gotchas¶
- "Running" isn't ready. The container is up long before SONiC's services are, and
configandvtyshdon't work until they are. Make the firstsetupline a readiness check, so every later line starts on a ready switch. no bgp ebgp-requires-policy. With it on (RFC 8212, FRR's default profile), an eBGP neighbor with no route-map comes up and exchanges nothing; see the FRR guide's gotchas. The example turns it off to stay short. With real policies, attach route-maps instead.- The image is big and not on a registry. About 230 MB to download and 1.92 GB once loaded. Cache it in CI.
- Port names differ between the topology and the switch:
eth1intopology.clab.yml,Ethernet0in SONiC commands, checks and scenarios. - Memory: about 300 MiB for the SONiC VS node once up (FAQ).