Skip to content

Commit 9936d08

Browse files
authored
Merge pull request #317 from mssonicbld/sonicbld/202603-merge
```<br>* 72384e2 - (HEAD -> 202603) Merge branch '202511' of https://github.com/sonic-net/sonic-utilities into 202603 (2026-04-21) [Sonic Automation] * df7ec22 - (origin/202511) Nokia-armhf config-reload with swss and sync restart (#4473) (2026-04-20) [mssonicbld]<br>```
2 parents 2b66f3b + 72384e2 commit 9936d08

2 files changed

Lines changed: 238 additions & 0 deletions

File tree

config/main.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,6 +1078,66 @@ def _wait_for_monit_service_monitored(service, timeout=10):
10781078
log.log_error("Monit monitor action for '{}' did not complete within {} seconds".format(service, timeout))
10791079

10801080

1081+
def get_device_name():
1082+
"""
1083+
Return the device/model id from /host/machine.conf onie_platform line.
1084+
Hardware SKU string, e.g. armhf-nokia_ixs7215_52x-r0.
1085+
"""
1086+
machine_conf = "/host/machine.conf"
1087+
try:
1088+
with open(machine_conf) as f:
1089+
for line in f:
1090+
if line.startswith("onie_platform="):
1091+
return line.strip().split("=", 1)[1]
1092+
except Exception:
1093+
return None
1094+
1095+
1096+
def get_mgmt_interface():
1097+
"""
1098+
Parse /etc/sonic/config_db.json to find the management interface name (e.g., eth0).
1099+
"""
1100+
try:
1101+
with open('/etc/sonic/config_db.json') as f:
1102+
config = json.load(f)
1103+
mgmt_entries = config.get("MGMT_INTERFACE", {})
1104+
if not mgmt_entries:
1105+
return None
1106+
# Example key: "eth0|10.3.141.10/24"
1107+
return list(mgmt_entries.keys())[0].split('|')[0]
1108+
except Exception:
1109+
# Valid - no mgmt interface in config_db.json
1110+
return None
1111+
1112+
1113+
def reset_mgmt_interface_if_usb_not_running():
1114+
"""
1115+
If the management interface is a USB device and not RUNNING,
1116+
bring it down and up with delay to trigger re-negotiation.
1117+
Failures are logged and ignored so reload is not aborted.
1118+
"""
1119+
iface = get_mgmt_interface()
1120+
if not iface:
1121+
return
1122+
if 'usb' not in os.path.realpath(f"/sys/class/net/{iface}/device"):
1123+
return
1124+
try:
1125+
with open(f"/sys/class/net/{iface}/operstate") as f:
1126+
operstate = f.read().strip()
1127+
if operstate == "up":
1128+
return # Already RUNNING
1129+
except Exception:
1130+
pass # Continue to attempt reset
1131+
1132+
click.echo("Reset USB-based mgmt interface for re-negotiation")
1133+
try:
1134+
subprocess.run(["ip", "link", "set", iface, "down"], check=True)
1135+
time.sleep(1.0)
1136+
subprocess.run(["ip", "link", "set", iface, "up"], check=True)
1137+
except (subprocess.CalledProcessError, OSError) as err:
1138+
log.log_warning("USB mgmt interface reset failed for {}: {}".format(iface, err))
1139+
1140+
10811141
def _restart_services():
10821142
last_interface_config_timestamp = get_service_finish_timestamp('interfaces-config')
10831143
last_networking_timestamp = get_service_finish_timestamp('networking')
@@ -1107,6 +1167,19 @@ def _restart_services():
11071167
click.echo("Reloading Monit configuration ...")
11081168
clicommon.run_command(['sudo', 'monit', 'reload'])
11091169

1170+
device_model = get_device_name()
1171+
if device_model == "armhf-nokia_ixs7215_52x-r0":
1172+
click.echo("ARMHF/Nokia-7215: force restart swss and syncd")
1173+
time.sleep(15)
1174+
clicommon.run_command(['sudo', 'systemctl', 'stop', 'swss'])
1175+
clicommon.run_command(['sudo', 'systemctl', 'stop', 'syncd'])
1176+
time.sleep(1)
1177+
clicommon.run_command(['sudo', 'systemctl', 'reset-failed', 'swss'])
1178+
clicommon.run_command(['sudo', 'systemctl', 'reset-failed', 'syncd'])
1179+
clicommon.run_command(['sudo', 'systemctl', 'restart', 'swss'])
1180+
1181+
reset_mgmt_interface_if_usb_not_running()
1182+
11101183
def _per_namespace_swss_ready(service_name):
11111184
out, _ = clicommon.run_command(['systemctl', 'show', str(service_name), '--property', 'ActiveState', '--value'], return_cmd=True)
11121185
if out.strip() != "active":
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"""
2+
Unit tests for Nokia 7215 / USB management restart helpers in config.main
3+
"""
4+
import builtins
5+
import json
6+
import subprocess
7+
from unittest import mock
8+
from unittest.mock import patch, mock_open
9+
10+
from config.main import (
11+
get_device_name,
12+
get_mgmt_interface,
13+
reset_mgmt_interface_if_usb_not_running,
14+
_restart_services,
15+
)
16+
17+
18+
def _open_mock_operstate(read_data):
19+
"""Patch only /operstate reads; delegate other opens to the real builtin."""
20+
21+
def _side_effect(path, *args, **kwargs):
22+
path_str = path if isinstance(path, str) else str(path)
23+
if path_str.replace("\\", "/").rstrip("/").endswith("operstate"):
24+
return mock_open(read_data=read_data)()
25+
return builtins.open(path, *args, **kwargs)
26+
27+
return _side_effect
28+
29+
30+
class TestGetDeviceName(object):
31+
def test_reads_onie_platform(self):
32+
content = "foo=1\nonie_platform=armhf-nokia_ixs7215_52x-r0\n"
33+
with patch("builtins.open", mock_open(read_data=content)):
34+
assert get_device_name() == "armhf-nokia_ixs7215_52x-r0"
35+
36+
def test_missing_file_returns_none(self):
37+
with patch("builtins.open", side_effect=OSError("no file")):
38+
assert get_device_name() is None
39+
40+
def test_no_onie_line_returns_none(self):
41+
with patch("builtins.open", mock_open(read_data="foo=bar\n")):
42+
assert get_device_name() is None
43+
44+
45+
class TestGetMgmtInterface(object):
46+
def test_parses_first_mgmt_key(self):
47+
cfg = {"MGMT_INTERFACE": {"eth0|10.0.0.1/24": {}}}
48+
with patch("builtins.open", mock_open(read_data=json.dumps(cfg))):
49+
assert get_mgmt_interface() == "eth0"
50+
51+
def test_empty_mgmt_returns_none(self):
52+
cfg = {"MGMT_INTERFACE": {}}
53+
with patch("builtins.open", mock_open(read_data=json.dumps(cfg))):
54+
assert get_mgmt_interface() is None
55+
56+
def test_invalid_json_returns_none(self):
57+
with patch("builtins.open", mock_open(read_data="not json")):
58+
assert get_mgmt_interface() is None
59+
60+
61+
class TestResetMgmtInterfaceIfUsbNotRunning(object):
62+
@patch("config.main.subprocess.run")
63+
@patch("config.main.get_mgmt_interface", return_value=None)
64+
def test_no_iface_no_subprocess(self, _gm, _sub):
65+
reset_mgmt_interface_if_usb_not_running()
66+
_sub.assert_not_called()
67+
68+
@patch("config.main.subprocess.run")
69+
@patch("config.main.os.path.realpath", return_value="/sys/devices/platform/eth")
70+
@patch("config.main.get_mgmt_interface", return_value="eth0")
71+
def test_skips_non_usb_device(self, _gm, _rp, _sub):
72+
reset_mgmt_interface_if_usb_not_running()
73+
_sub.assert_not_called()
74+
75+
@patch("config.main.subprocess.run")
76+
@patch("builtins.open", side_effect=_open_mock_operstate("up\n"))
77+
@patch("config.main.os.path.realpath", return_value="/sys/bus/usb/devices/usb1")
78+
@patch("config.main.get_mgmt_interface", return_value="eth0")
79+
def test_skips_when_operstate_up(self, _gm, _rp, _op, _sub):
80+
reset_mgmt_interface_if_usb_not_running()
81+
_sub.assert_not_called()
82+
83+
@patch("config.main.subprocess.run")
84+
@patch("config.main.click.echo")
85+
@patch("builtins.open", side_effect=_open_mock_operstate("down\n"))
86+
@patch("config.main.os.path.realpath", return_value="/sys/bus/usb/devices/usb1")
87+
@patch("config.main.get_mgmt_interface", return_value="eth0")
88+
def test_runs_ip_link_when_usb_and_not_up(self, _gm, _rp, _op, _echo, sub):
89+
reset_mgmt_interface_if_usb_not_running()
90+
sub.assert_any_call(["ip", "link", "set", "eth0", "down"], check=True)
91+
sub.assert_any_call(["ip", "link", "set", "eth0", "up"], check=True)
92+
93+
@patch("config.main.log.log_warning")
94+
@patch(
95+
"config.main.subprocess.run",
96+
side_effect=subprocess.CalledProcessError(1, ["ip"]),
97+
)
98+
@patch("config.main.click.echo")
99+
@patch("builtins.open", side_effect=_open_mock_operstate("down\n"))
100+
@patch("config.main.os.path.realpath", return_value="/sys/bus/usb/devices/usb1")
101+
@patch("config.main.get_mgmt_interface", return_value="eth0")
102+
def test_logs_warning_on_subprocess_failure(
103+
self, _gm, _rp, _op, _echo, _sub, log_warning
104+
):
105+
reset_mgmt_interface_if_usb_not_running()
106+
log_warning.assert_called_once()
107+
assert "eth0" in str(log_warning.call_args)
108+
109+
110+
class TestRestartServicesNokiaExtension(object):
111+
@patch("config.main.reset_mgmt_interface_if_usb_not_running")
112+
@patch("config.main.get_device_name", return_value=None)
113+
@patch("config.main.clicommon.run_command")
114+
@patch("config.main.subprocess.check_call")
115+
@patch("config.main.wait_service_restart_finish")
116+
@patch("config.main.get_service_finish_timestamp", return_value=0)
117+
@patch("config.main.click.echo")
118+
@patch("config.main._wait_for_monit_service_monitored")
119+
def test_always_calls_usb_mgmt_reset(
120+
self,
121+
_monit,
122+
_echo,
123+
_gst,
124+
_wsrf,
125+
_check,
126+
run_cmd,
127+
get_dev,
128+
reset_usb,
129+
):
130+
_restart_services()
131+
reset_usb.assert_called_once()
132+
get_dev.assert_called()
133+
134+
@patch("config.main.reset_mgmt_interface_if_usb_not_running")
135+
@patch("config.main.time.sleep", autospec=True)
136+
@patch("config.main.get_device_name", return_value="armhf-nokia_ixs7215_52x-r0")
137+
@patch("config.main.clicommon.run_command")
138+
@patch("config.main.subprocess.check_call")
139+
@patch("config.main.wait_service_restart_finish")
140+
@patch("config.main.get_service_finish_timestamp", return_value=0)
141+
@patch("config.main.click.echo")
142+
@patch("config.main._wait_for_monit_service_monitored")
143+
def test_nokia7215_runs_swss_syncd_restart(
144+
self,
145+
_monit,
146+
echo,
147+
_gst,
148+
_wsrf,
149+
_check,
150+
run_cmd,
151+
get_dev,
152+
_sleep,
153+
reset_usb,
154+
):
155+
_restart_services()
156+
reset_usb.assert_called_once()
157+
stop_swss = mock.call(["sudo", "systemctl", "stop", "swss"])
158+
stop_syncd = mock.call(["sudo", "systemctl", "stop", "syncd"])
159+
assert stop_swss in run_cmd.call_args_list
160+
assert stop_syncd in run_cmd.call_args_list
161+
restart_swss = mock.call(["sudo", "systemctl", "restart", "swss"])
162+
assert restart_swss in run_cmd.call_args_list
163+
echo.assert_any_call(
164+
"ARMHF/Nokia-7215: force restart swss and syncd"
165+
)

0 commit comments

Comments
 (0)