Skip to content

Commit 0b2ded0

Browse files
committed
[tempershow]: Read SFP temperature from xcvrd-managed tables
thermalctld no longer publishes per-SFP entries into TEMPERATURE_INFO. Update tempershow to additionally read SFP temperatures from the xcvrd-managed TRANSCEIVER_DOM_TEMPERATURE table, with thresholds from TRANSCEIVER_DOM_THRESHOLD: High TH <- temphighwarning Low TH <- templowwarning Crit High TH <- temphighalarm Crit Low TH <- templowalarm Existing TEMPERATURE_INFO consumers (chassis/PSU/fan sensors) continue to be displayed. Missing fields default to N/A instead of raising KeyError; the table key glob is also tightened to '<table>|*'. Signed-off-by: Vasundhara Volam <vvolam@microsoft.com>
1 parent f62b8ec commit 0b2ded0

1 file changed

Lines changed: 176 additions & 37 deletions

File tree

scripts/tempershow

Lines changed: 176 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,23 @@
55
"""
66
import argparse
77
import json
8+
from datetime import datetime
89

910
from tabulate import tabulate
1011
from swsscommon.swsscommon import SonicV2Connector
1112
from natsort import natsorted
1213

14+
try:
15+
from sonic_platform_base.sonic_sfp.sfputilhelper import SfpUtilHelper
16+
except ImportError:
17+
SfpUtilHelper = None
18+
19+
try:
20+
from sonic_py_common import device_info, multi_asic
21+
except ImportError:
22+
device_info = None
23+
multi_asic = None
24+
1325

1426
header = ['Sensor', 'Temperature', 'High TH', 'Low TH', 'Crit High TH', 'Crit Low TH', 'Warning', 'Timestamp']
1527

@@ -22,59 +34,186 @@ CRIT_HIGH_THRESH_FIELD_NAME = 'critical_high_threshold'
2234
CRIT_LOW_THRESH_FIELD_NAME = 'critical_low_threshold'
2335
WARNING_STATUS_FIELD_NAME = 'warning_status'
2436

37+
# SFP/transceiver temperature is published by xcvrd (not thermalctld) into
38+
# the dedicated TRANSCEIVER_DOM_TEMPERATURE table; thresholds come from
39+
# TRANSCEIVER_DOM_THRESHOLD; per-flag warning/alarm state comes from
40+
# TRANSCEIVER_DOM_FLAG. Field names are defined by xcvrd.
41+
SFP_TEMPER_TABLE_NAME = 'TRANSCEIVER_DOM_TEMPERATURE'
42+
SFP_THRESHOLD_TABLE_NAME = 'TRANSCEIVER_DOM_THRESHOLD'
43+
SFP_FLAG_TABLE_NAME = 'TRANSCEIVER_DOM_FLAG'
44+
SFP_DISPLAY_TIME_FORMAT = '%Y%m%d %H:%M:%S'
45+
SFP_TEMPER_FIELD_NAME = 'temperature'
46+
SFP_TEMP_HIGH_WARN_FIELD = 'temphighwarning'
47+
SFP_TEMP_LOW_WARN_FIELD = 'templowwarning'
48+
SFP_TEMP_HIGH_ALARM_FIELD = 'temphighalarm'
49+
SFP_TEMP_LOW_ALARM_FIELD = 'templowalarm'
50+
SFP_TEMP_FLAG_FIELDS = (
51+
SFP_TEMP_HIGH_WARN_FIELD,
52+
SFP_TEMP_LOW_WARN_FIELD,
53+
SFP_TEMP_HIGH_ALARM_FIELD,
54+
SFP_TEMP_LOW_ALARM_FIELD,
55+
)
56+
57+
NA = 'N/A'
58+
2559

2660
class TemperShow(object):
2761
def __init__(self):
2862
self.db = SonicV2Connector(host="127.0.0.1")
2963
self.db.connect(self.db.STATE_DB)
64+
self.sfp_util = self._init_sfp_util_helper()
3065

31-
def show(self, output_json):
32-
keys = self.db.keys(self.db.STATE_DB, TEMPER_TABLE_NAME + '*')
33-
if not keys:
34-
print('Thermal Not detected\n')
35-
return
66+
def _init_sfp_util_helper(self):
67+
"""Initialize SfpUtilHelper for logical-port -> physical-port mapping.
3668
37-
table = []
38-
json_output = []
69+
Returns None if the helper or platform info is unavailable; SFP rows
70+
will then fall back to displaying the logical port name.
71+
"""
72+
if SfpUtilHelper is None or device_info is None or multi_asic is None:
73+
return None
74+
try:
75+
sfp_util = SfpUtilHelper()
76+
if multi_asic.is_multi_asic():
77+
_, hwsku_path = device_info.get_paths_to_platform_and_hwsku_dirs()
78+
sfp_util.read_all_porttab_mappings(hwsku_path, multi_asic.get_num_asics())
79+
else:
80+
port_config_file_path = device_info.get_path_to_port_config_file()
81+
sfp_util.read_porttab_mappings(port_config_file_path, 0)
82+
return sfp_util
83+
except Exception:
84+
return None
85+
86+
def _sfp_display_name(self, port_name):
87+
"""Map a logical port name (e.g. 'Ethernet0') to 'xSFP module <N> Temp'.
88+
89+
Falls back to the logical port name if the mapping is unavailable.
90+
This matches the legacy thermalctld naming convention so existing
91+
consumers/operators see the same sensor labels.
92+
"""
93+
if self.sfp_util is None:
94+
return port_name
95+
try:
96+
physical = self.sfp_util.get_logical_to_physical(port_name)
97+
if physical and len(physical) > 0 and physical[0] is not None:
98+
# physical port index from SfpUtilHelper is already 1-based
99+
return 'xSFP module {} Temp'.format(physical[0])
100+
except Exception:
101+
pass
102+
return port_name
103+
104+
def _collect_platform_sensors(self):
105+
"""Collect chassis/PSU/fan thermal sensors from TEMPERATURE_INFO."""
106+
rows = []
107+
keys = self.db.keys(self.db.STATE_DB, TEMPER_TABLE_NAME + '|*')
108+
if not keys:
109+
return rows
39110
for key in natsorted(keys):
40111
key_list = key.split('|')
41-
if len(key_list) != 2: # error data in DB, log it and ignore
112+
if len(key_list) != 2:
42113
print('Warn: Invalid key in table {}: {}'.format(TEMPER_TABLE_NAME, key))
43114
continue
44-
45115
name = key_list[1]
46-
data_dict = self.db.get_all(self.db.STATE_DB, key)
47-
if output_json:
48-
json_output.append({
49-
"Sensor": name,
50-
"Temperature": data_dict[TEMPER_FIELD_NAME],
51-
"High_TH": data_dict[HIGH_THRESH_FIELD_NAME],
52-
"Low_TH": data_dict[LOW_THRESH_FIELD_NAME],
53-
"Crit_High_TH": data_dict[CRIT_HIGH_THRESH_FIELD_NAME],
54-
"Crit_Low_TH": data_dict[CRIT_LOW_THRESH_FIELD_NAME],
55-
"Warning": data_dict[WARNING_STATUS_FIELD_NAME],
56-
"Timestamp": data_dict[TIMESTAMP_FIELD_NAME]
57-
})
116+
data_dict = self.db.get_all(self.db.STATE_DB, key) or {}
117+
rows.append({
118+
'name': name,
119+
'temperature': data_dict.get(TEMPER_FIELD_NAME, NA),
120+
'high_th': data_dict.get(HIGH_THRESH_FIELD_NAME, NA),
121+
'low_th': data_dict.get(LOW_THRESH_FIELD_NAME, NA),
122+
'crit_high_th': data_dict.get(CRIT_HIGH_THRESH_FIELD_NAME, NA),
123+
'crit_low_th': data_dict.get(CRIT_LOW_THRESH_FIELD_NAME, NA),
124+
'warning': data_dict.get(WARNING_STATUS_FIELD_NAME, NA),
125+
'timestamp': data_dict.get(TIMESTAMP_FIELD_NAME, NA),
126+
})
127+
return rows
128+
129+
def _collect_sfp_sensors(self):
130+
"""Collect SFP/transceiver temperatures from xcvrd-managed tables.
131+
132+
Map xcvrd threshold fields onto tempershow's columns:
133+
high_th <- temphighwarning
134+
low_th <- templowwarning
135+
crit_high_th <- temphighalarm
136+
crit_low_th <- templowalarm
137+
138+
Warning column is True if any of the temperature high/low
139+
warning/alarm flags in TRANSCEIVER_DOM_FLAG are asserted.
140+
"""
141+
rows = []
142+
keys = self.db.keys(self.db.STATE_DB, SFP_TEMPER_TABLE_NAME + '|*')
143+
if not keys:
144+
return rows
145+
timestamp = datetime.now().strftime(SFP_DISPLAY_TIME_FORMAT)
146+
for key in natsorted(keys):
147+
key_list = key.split('|')
148+
if len(key_list) != 2:
149+
print('Warn: Invalid key in table {}: {}'.format(SFP_TEMPER_TABLE_NAME, key))
150+
continue
151+
port = key_list[1]
152+
temp_dict = self.db.get_all(self.db.STATE_DB, key) or {}
153+
thr_dict = self.db.get_all(
154+
self.db.STATE_DB,
155+
'{}|{}'.format(SFP_THRESHOLD_TABLE_NAME, port)) or {}
156+
flag_dict = self.db.get_all(
157+
self.db.STATE_DB,
158+
'{}|{}'.format(SFP_FLAG_TABLE_NAME, port)) or {}
159+
rows.append({
160+
'name': self._sfp_display_name(port),
161+
'temperature': temp_dict.get(SFP_TEMPER_FIELD_NAME, NA),
162+
'high_th': thr_dict.get(SFP_TEMP_HIGH_WARN_FIELD, NA),
163+
'low_th': thr_dict.get(SFP_TEMP_LOW_WARN_FIELD, NA),
164+
'crit_high_th': thr_dict.get(SFP_TEMP_HIGH_ALARM_FIELD, NA),
165+
'crit_low_th': thr_dict.get(SFP_TEMP_LOW_ALARM_FIELD, NA),
166+
'warning': self._derive_sfp_warning(flag_dict),
167+
'timestamp': timestamp,
168+
})
169+
return rows
170+
171+
@staticmethod
172+
def _derive_sfp_warning(flag_dict):
173+
"""Return True/False/N/A based on temp flags in TRANSCEIVER_DOM_FLAG.
174+
175+
True - any of temphigh/lowwarning/alarm is asserted
176+
False - all four flags are present and de-asserted
177+
N/A - none of the four flags are present
178+
"""
179+
if not flag_dict:
180+
return NA
181+
seen = False
182+
for field in SFP_TEMP_FLAG_FIELDS:
183+
value = flag_dict.get(field)
184+
if value is None:
185+
continue
186+
seen = True
187+
if str(value).strip().lower() == 'true':
188+
return True
189+
return False if seen else NA
190+
191+
def show(self, output_json):
192+
rows = self._collect_platform_sensors() + self._collect_sfp_sensors()
193+
if not rows:
194+
print('Thermal Not detected\n')
195+
return
58196

59-
else:
60-
table.append((name,
61-
data_dict[TEMPER_FIELD_NAME],
62-
data_dict[HIGH_THRESH_FIELD_NAME],
63-
data_dict[LOW_THRESH_FIELD_NAME],
64-
data_dict[CRIT_HIGH_THRESH_FIELD_NAME],
65-
data_dict[CRIT_LOW_THRESH_FIELD_NAME],
66-
data_dict[WARNING_STATUS_FIELD_NAME],
67-
data_dict[TIMESTAMP_FIELD_NAME]
68-
))
69-
70197
if output_json:
198+
json_output = [{
199+
"Sensor": r['name'],
200+
"Temperature": r['temperature'],
201+
"High_TH": r['high_th'],
202+
"Low_TH": r['low_th'],
203+
"Crit_High_TH": r['crit_high_th'],
204+
"Crit_Low_TH": r['crit_low_th'],
205+
"Warning": r['warning'],
206+
"Timestamp": r['timestamp'],
207+
} for r in rows]
71208
print(json.dumps(json_output, indent=2))
72-
elif table:
73-
print(tabulate(table, header, tablefmt='simple', stralign='right'))
74-
else:
75-
print('No temperature data available\n')
209+
return
210+
211+
table = [(r['name'], r['temperature'], r['high_th'], r['low_th'],
212+
r['crit_high_th'], r['crit_low_th'], r['warning'],
213+
r['timestamp']) for r in rows]
214+
print(tabulate(table, header, tablefmt='simple', stralign='right'))
215+
76216

77-
78217
if __name__ == "__main__":
79218
parser = argparse.ArgumentParser(description='Display the temperature Sensor information',
80219
formatter_class=argparse.RawTextHelpFormatter,

0 commit comments

Comments
 (0)