Skip to content

Commit cd70272

Browse files
sai_test: add opt-in SIMULATE_SONIC to emulate SONiC teamd/IntfMgr netdevs for standalone benches (#2316)
Context / motivation Part of the SAIVPP unit-test framework (see the sonic-sairedis docker-sai-test-vpp harness). When the OCP sai_test T0 suite runs against a SAI backend on a standalone bench with no SONiC control plane, there is no teamd to create PortChannel netdevs and no IntfMgr to assign LAG/SVI interface IPs — which some backends (e.g. the VPP virtual switch) rely on for LAG bring-up and routed-to-LAG/SVI forwarding. This adds an opt-in way for the test setup to emulate that. What this change does test/sai_test/config/simulate_sonic.py (new). Opt-in helper, active only when SIMULATE_SONIC=1; every entry point is a no-op otherwise. test/sai_test/config/lag_configer.py — create_lag(). When enabled, create the PortChannel<N> netdev teamd would provide, before the SAI LAG create. test/sai_test/config/route_configer.py — create_router_interface(). When enabled, assign the connected IPs IntfMgr would set on a LAG or VLAN(SVI) router interface. Vendor-neutral by construction. PortChannel netdevs use generic Linux ip … type bond; LAG RIF IPs use ip addr add. A VLAN SVI has no host-interface netdev to mirror from, so its address is programmed via command templates supplied by the caller (SVI_RIF_SET_IP_CMD / SVI_RIF_PROBE_CMD, with {ifname}/{addr} placeholders) — no backend-specific tooling is hardcoded in sai_test. All interface-name prefixes and address patterns are env-overridable. Scope / risk 3 files (one new module + two setUp-path hooks) — test-config helpers only; no change to SAI/backend code or packet semantics. Fully opt-in with default-off: with SIMULATE_SONIC unset, all entry points no-op, so real-HW and other OCP consumers are unchanged unless they opt in. Dependencies None. Independent of #2299 / #2300 / #2301 (edits different functions); targets master.
1 parent 64d4b84 commit cd70272

3 files changed

Lines changed: 197 additions & 0 deletions

File tree

test/sai_test/config/lag_configer.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ def create_lag(self, lag_port_idxs):
7878
Lag: lag object
7979
"""
8080
lag: Lag = Lag(None, [], [])
81+
lag_index = len(self.test_obj.dut.lag_list)
82+
try:
83+
from config import simulate_sonic
84+
except ImportError:
85+
simulate_sonic = None
86+
if simulate_sonic is not None:
87+
simulate_sonic.ensure_portchannel(lag_index)
8188
lag_oid = sai_thrift_create_lag(self.client)
8289
lag.oid = lag_oid
8390
self.create_lag_member(lag, lag_port_idxs)

test/sai_test/config/route_configer.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,8 +549,25 @@ def create_router_interface(self, net_interface: route_item, virtual_router=None
549549
if not self.test_obj.dut.rif_list:
550550
self.test_obj.dut.rif_list = []
551551
self.test_obj.dut.rif_list.append(rif)
552+
self._simulate_sonic_assign_rif_ips(net_interface)
552553
return net_interface.rif_list[-1]
553554

555+
def _simulate_sonic_assign_rif_ips(self, net_interface):
556+
"""When SIMULATE_SONIC=1, assign the connected IPs IntfMgr would set on a
557+
LAG/SVI router interface (no-op otherwise)."""
558+
try:
559+
from config import simulate_sonic
560+
except ImportError:
561+
return
562+
if isinstance(net_interface, Lag):
563+
try:
564+
lag_index = self.test_obj.dut.lag_list.index(net_interface)
565+
except ValueError:
566+
return
567+
simulate_sonic.assign_lag_rif_ips(lag_index)
568+
elif isinstance(net_interface, Vlan):
569+
simulate_sonic.assign_svi_rif_ips(net_interface.vlan_id)
570+
554571
def create_nexthop_by_rif(self, rif, nexthop_device: Device):
555572
"""
556573
Create nexthop by bridge port index for a port.
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Copyright (c) 2026 Microsoft Open Technologies, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR
8+
# CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT
9+
# LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS
10+
# FOR A PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT.
11+
#
12+
# See the Apache Version 2.0 License for specific language governing
13+
# permissions and limitations under the License.
14+
#
15+
# Microsoft would like to thank the following companies for their review and
16+
# assistance with these files: Intel Corporation, Mellanox Technologies Ltd,
17+
# Dell Products, L.P., Facebook, Inc., Marvell International Ltd.
18+
#
19+
#
20+
21+
# Optional SONiC control-plane simulation for standalone sai_test benches.
22+
#
23+
# In a real SONiC device teamd creates PortChannel netdevs and IntfMgr assigns the
24+
# interface IPs that the SAI backend mirrors onto LAG/SVI router interfaces. A
25+
# standalone sai_test bench (no teamd / IntfMgr) has neither, so LAG bring-up and
26+
# routed-to-LAG/SVI forwarding can fail. When SIMULATE_SONIC=1 the config helpers
27+
# call into this module to emulate that setup from the test's setUp path.
28+
#
29+
# This is opt-in and default-preserving: with SIMULATE_SONIC unset (the default) all
30+
# entry points are no-ops, so real ASICs and other harnesses are unaffected. All
31+
# interface names and address patterns are environment-overridable so the defaults
32+
# can be retargeted without code changes. This module is backend-neutral: it uses
33+
# standard Linux "ip" for PortChannel netdevs and LAG router-interface IPs, and for
34+
# a VLAN (SVI) router interface -- which has no host-interface netdev to mirror from
35+
# -- it runs the command templates the harness provides via SVI_RIF_SET_IP_CMD (and
36+
# optional SVI_RIF_PROBE_CMD), so no backend-specific tooling is hardcoded here.
37+
38+
import logging
39+
import os
40+
import shlex
41+
import subprocess
42+
import time
43+
44+
_logger = logging.getLogger(__name__)
45+
46+
47+
def enabled():
48+
return os.environ.get("SIMULATE_SONIC", "0") == "1"
49+
50+
51+
def _run(cmd, check=False, quiet=False):
52+
result = subprocess.run(cmd, check=False, capture_output=True, text=True)
53+
if result.returncode != 0 and not quiet:
54+
msg = "command failed (rc={}): {}".format(result.returncode, cmd)
55+
stderr = result.stderr.strip() if result.stderr else ""
56+
if stderr:
57+
msg = "{} stderr: {}".format(msg, stderr)
58+
if check:
59+
_logger.error(msg)
60+
result.check_returncode()
61+
else:
62+
_logger.warning(msg)
63+
elif check:
64+
result.check_returncode()
65+
return result
66+
67+
68+
def _mtu():
69+
return int(os.environ.get("MTU", "9100"))
70+
71+
72+
def _templated(cmd_tmpl, **kw):
73+
"""Split a command template and substitute {ifname}/{addr} placeholders."""
74+
return shlex.split(cmd_tmpl.format(**kw))
75+
76+
77+
def ensure_portchannel(lag_index):
78+
"""Create the PortChannel<lag_index> netdev teamd would provide for a LAG."""
79+
if not enabled():
80+
return
81+
82+
pc_if = "PortChannel{}".format(lag_index)
83+
mtu = _mtu()
84+
85+
if _run(["ip", "link", "show", pc_if], quiet=True).returncode != 0:
86+
if _run(["ip", "link", "add", pc_if, "type", "bond"]).returncode != 0:
87+
_run(["ip", "link", "add", pc_if, "type", "dummy"], check=False)
88+
89+
_run(["ip", "link", "set", "dev", pc_if, "mtu", str(mtu)], check=False)
90+
_run(["ip", "link", "set", "dev", pc_if, "up"], check=False)
91+
92+
93+
def assign_lag_rif_ips(lag_index, retries=20, interval=0.3):
94+
"""Assign the connected IPs IntfMgr would put on a LAG router interface.
95+
96+
The IP is placed on the backend's LAG host-interface netdev (default "be<N>"),
97+
which the backend mirrors onto the LAG router interface.
98+
"""
99+
if not enabled() or os.environ.get("LAG_RIF_IPS", "1") != "1":
100+
return
101+
102+
be_prefix = os.environ.get("LAG_BE_TAP_PREFIX", "be")
103+
v4_pattern = os.environ.get("LAG_RIF_IPV4_PATTERN", "10.1.%d.1/24")
104+
v6_pattern = os.environ.get("LAG_RIF_IPV6_PATTERN", "fc00:1::%d:1/112")
105+
subnet_id = lag_index + 1
106+
be_if = "{}{}".format(be_prefix, lag_index)
107+
v4_addr = v4_pattern % subnet_id
108+
v6_addr = v6_pattern % subnet_id
109+
110+
for _ in range(retries):
111+
if _run(["ip", "link", "show", be_if], quiet=True).returncode == 0:
112+
break
113+
time.sleep(interval)
114+
else:
115+
return
116+
117+
disable_v6 = "/proc/sys/net/ipv6/conf/{}/disable_ipv6".format(be_if)
118+
accept_dad = "/proc/sys/net/ipv6/conf/{}/accept_dad".format(be_if)
119+
try:
120+
with open(disable_v6, "w", encoding="ascii") as fh:
121+
fh.write("0")
122+
with open(accept_dad, "w", encoding="ascii") as fh:
123+
fh.write("0")
124+
except OSError:
125+
pass
126+
127+
_run(["ip", "addr", "add", v4_addr, "dev", be_if], check=False)
128+
_run(["ip", "-6", "addr", "add", v6_addr, "dev", be_if, "nodad"], check=False)
129+
130+
131+
def _svi_group_id(vlan_id):
132+
for entry in os.environ.get("SVI_RIF_VLANS", "10:1 20:2").split():
133+
vid, gid = entry.split(":", 1)
134+
if int(vid) == int(vlan_id):
135+
return int(gid)
136+
return int(vlan_id)
137+
138+
139+
def assign_svi_rif_ips(vlan_id, retries=20, interval=0.3):
140+
"""Assign the connected IPs IntfMgr would put on a VLAN (SVI) router interface.
141+
142+
A VLAN SVI has no host-interface netdev to mirror from, so the IP must be set on
143+
the backend interface directly. The backend-specific commands are injected by the
144+
harness as templates (with {ifname}/{addr} placeholders): SVI_RIF_SET_IP_CMD sets
145+
the address, and the optional SVI_RIF_PROBE_CMD waits until the interface exists.
146+
With SVI_RIF_SET_IP_CMD unset this is a no-op, keeping this module backend-neutral.
147+
"""
148+
if not enabled() or os.environ.get("SVI_RIF_IPS", "1") != "1":
149+
return
150+
151+
set_ip_cmd = os.environ.get("SVI_RIF_SET_IP_CMD")
152+
if not set_ip_cmd:
153+
return
154+
155+
probe_cmd = os.environ.get("SVI_RIF_PROBE_CMD")
156+
bvi_prefix = os.environ.get("SVI_BVI_PREFIX", "bvi")
157+
v4_pattern = os.environ.get("SVI_RIF_IPV4_PATTERN", "192.168.%d.1/24")
158+
v6_pattern = os.environ.get("SVI_RIF_IPV6_PATTERN", "fc02::%d:1/112")
159+
group_id = _svi_group_id(vlan_id)
160+
bvi_if = "{}{}".format(bvi_prefix, vlan_id)
161+
v4_addr = v4_pattern % group_id
162+
v6_addr = v6_pattern % group_id
163+
164+
if probe_cmd:
165+
for _ in range(retries):
166+
if _run(_templated(probe_cmd, ifname=bvi_if, addr=""), quiet=True).returncode == 0:
167+
break
168+
time.sleep(interval)
169+
else:
170+
return
171+
172+
for addr in (v4_addr, v6_addr):
173+
_run(_templated(set_ip_cmd, ifname=bvi_if, addr=addr), check=False)

0 commit comments

Comments
 (0)