Skip to content

Commit c37b846

Browse files
wiperilizhijianrd
andauthored
[consutil, config]: add supporting command for console monitor utility (#4208)
What I did Add new modification and new commands to support console monitor feature. HLD How I did it Modify show line commands. Display 2 new column, Oper State and State Duration. Add new commands config console heartbeat {enable/disable}. Provide configuration ability of console monitor dte (heartbeat sending) service. How to verify it Manually tested on physical testbed. --------- Signed-off-by: cliffchen <t-cliffchen+github@microsoft.com> Signed-off-by: Zhijian Li <zhijianli@microsoft.com> Co-authored-by: Zhijian Li <zhijianli@microsoft.com>
1 parent 39732bc commit c37b846

4 files changed

Lines changed: 301 additions & 14 deletions

File tree

config/console.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,27 @@ def disable_console_switch(db):
5757

5858

5959
#
60+
# 'console heartbeat' group ('config console heartbeat ...')
61+
#
62+
@console.command('heartbeat')
63+
@clicommon.pass_db
64+
@click.argument('mode', metavar='<mode>', required=True, type=click.Choice(["enable", "disable"]))
65+
def update_console_heartbeat(db, mode):
66+
"""Enable/Disable console heartbeat on controlled device (DTE side)"""
67+
config_db = ValidatedConfigDBConnector(db.cfgdb)
68+
69+
table = "CONSOLE_SWITCH"
70+
dataKey1 = 'controlled_device'
71+
dataKey2 = 'enabled'
72+
73+
data = {dataKey2: "yes" if mode == "enable" else "no"}
74+
try:
75+
config_db.mod_entry(table, dataKey1, data)
76+
except ValueError as e:
77+
ctx = click.get_current_context()
78+
ctx.fail("Invalid ConfigDB. Error: {}".format(e))
79+
80+
6081
# 'console default_escape' group ('config console default_escape A|B|...')
6182
#
6283
@console.command('default_escape')

consutil/lib.py

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import re
1010
import subprocess
1111
import sys
12+
import time
13+
1214

1315
import click
1416
from sonic_py_common import device_info
@@ -39,6 +41,8 @@
3941
STATE_KEY = "state"
4042
PID_KEY = "pid"
4143
START_TIME_KEY = "start_time"
44+
OPER_STATE_KEY = "oper_state"
45+
LAST_STATE_CHANGE_KEY = "last_state_change"
4246

4347
BUSY_FLAG = "busy"
4448
IDLE_FLAG = "idle"
@@ -49,6 +53,8 @@
4953

5054
UDEV_PREFIX_CONF_FILENAME = "udevprefix.conf"
5155

56+
PTY_SYMLINK_SUFFIX = "-PTS"
57+
5258
TIMEOUT_SEC = 0.2
5359

5460
class ConsolePortProvider(object):
@@ -176,6 +182,48 @@ def busy(self):
176182
def session_pid(self):
177183
return self.cur_state[PID_KEY] if PID_KEY in self.cur_state else None
178184

185+
@property
186+
def oper_state(self):
187+
return self.cur_state[OPER_STATE_KEY] if OPER_STATE_KEY in self.cur_state else None
188+
189+
@property
190+
def last_state_change(self):
191+
return self.cur_state[LAST_STATE_CHANGE_KEY] if LAST_STATE_CHANGE_KEY in self.cur_state else None
192+
193+
@property
194+
def state_duration(self):
195+
"""Calculate and format the duration since last state change.
196+
Format: XdXhXmXs (only shows non-zero parts)
197+
"""
198+
if not self.last_state_change:
199+
return None
200+
try:
201+
ts = int(self.last_state_change)
202+
now = int(time.time())
203+
diff = now - ts
204+
if diff < 0:
205+
return None
206+
207+
# Calculate time components
208+
days, remainder = divmod(diff, 24 * 3600)
209+
hours, remainder = divmod(remainder, 3600)
210+
minutes, seconds = divmod(remainder, 60)
211+
212+
# Build formatted string, only include non-zero parts for d/h/m, always show seconds
213+
parts = []
214+
if days > 0:
215+
parts.append(f"{days}d")
216+
if hours > 0 or days > 0:
217+
parts.append(f"{hours}h")
218+
if minutes > 0 or hours > 0 or days > 0:
219+
parts.append(f"{minutes}m")
220+
221+
parts.append(f"{seconds}s") # Always show seconds
222+
223+
return "".join(parts)
224+
except (ValueError, OSError):
225+
return None
226+
179227
@property
180228
def session_start_date(self):
181229
return self.cur_state[START_TIME_KEY] if START_TIME_KEY in self.cur_state else None
@@ -201,8 +249,9 @@ def connect(self):
201249
# build and start picocom command
202250
flow_cmd = "h" if self.flow_control else "n"
203251
escape_cmd = "-e {}".format(self.escape_char) if self.escape_char else ""
204-
cmd = "picocom {} -b {} -f {} {}{}".format(escape_cmd, self.baud, flow_cmd,
205-
SysInfoProvider.DEVICE_PREFIX, self.line_num)
252+
cmd = "picocom {} -b {} -f {} {}{}{}".format(
253+
escape_cmd, self.baud, flow_cmd,
254+
SysInfoProvider.DEVICE_PREFIX, self.line_num, PTY_SYMLINK_SUFFIX)
206255

207256
# start connection
208257
try:
@@ -312,7 +361,7 @@ def list_console_ttys():
312361
cmd = ["bash", "-c", "ls " + SysInfoProvider.DEVICE_PREFIX + "*"]
313362
output, _ = SysInfoProvider.run_command(cmd, abort=False)
314363
ttys = output.split('\n')
315-
ttys = list([dev for dev in ttys if re.match(SysInfoProvider.DEVICE_PREFIX + r"\d+", dev) != None])
364+
ttys = list([dev for dev in ttys if re.match(SysInfoProvider.DEVICE_PREFIX + r"\d+$", dev)])
316365
return ttys
317366

318367
@staticmethod
@@ -345,8 +394,8 @@ def _parse_processes_info(output):
345394
regex_date = r"([A-Z][a-z]{2} [A-Z][a-z]{2} [\d ]\d \d{2}:\d{2}:\d{2} \d{4})"
346395
# matches any characters ending in minicom or picocom,
347396
# then a space and any chars followed by /dev/ttyUSB<any digits>,
348-
# then a space and any chars
349-
regex_cmd = r".*(?:(?:mini)|(?:pico))com .*" + SysInfoProvider.DEVICE_PREFIX + r"(\d+)(?: .*)?"
397+
# then any chars
398+
regex_cmd = r".*(?:(?:mini)|(?:pico))com .*" + SysInfoProvider.DEVICE_PREFIX + r"(\d+).*"
350399
regex_process = re.compile(r"^" + regex_pid + r" " + regex_date + r" " + regex_cmd + r"$")
351400

352401
console_processes = {}
@@ -378,10 +427,18 @@ def update_state(self, line_num, state, pid="", date=""):
378427
self._state_db.set(self._state_db.STATE_DB, key, STATE_KEY, state)
379428
self._state_db.set(self._state_db.STATE_DB, key, PID_KEY, pid)
380429
self._state_db.set(self._state_db.STATE_DB, key, START_TIME_KEY, date)
430+
431+
# Read existing oper_state and last_state_change from STATE_DB
432+
existing_data = self._state_db.get_all(self._state_db.STATE_DB, key)
433+
oper_state = existing_data.get(OPER_STATE_KEY, "") if existing_data else ""
434+
last_state_change = existing_data.get(LAST_STATE_CHANGE_KEY, "") if existing_data else ""
435+
381436
return {
382437
STATE_KEY: state,
383438
PID_KEY: pid,
384-
START_TIME_KEY: date
439+
START_TIME_KEY: date,
440+
OPER_STATE_KEY: oper_state,
441+
LAST_STATE_CHANGE_KEY: last_state_change
385442
}
386443

387444
class InvalidConfigurationError(Exception):

consutil/main.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,22 +44,27 @@ def show(db, brief):
4444
ports.sort(key=lambda p: int(p.line_num))
4545

4646
# set table header style
47-
header = ["Line", "Baud", "Flow Control", "PID", "Start Time", "Device"]
47+
header = ["Line", "Baud", "Flow Control", "PID", "Start Time", "Device", "Oper State", "State Duration"]
4848
body = []
4949
for port in ports:
5050
# runtime information
5151
busy = "*" if port.busy else " "
5252
pid = port.session_pid if port.session_pid else "-"
5353
date = port.session_start_date if port.session_start_date else "-"
54-
baud = port.baud
54+
baud = port.baud if port.baud else "-"
5555
flow_control = "Enabled" if port.flow_control else "Disabled"
56+
remote_device = port.remote_device if port.remote_device else "-"
57+
oper_state = port.oper_state if port.oper_state else "-"
58+
state_duration = port.state_duration if port.state_duration else "-"
5659
body.append([
5760
busy+port.line_num,
5861
baud if baud else "-",
5962
flow_control,
6063
pid if pid else "-",
6164
date if date else "-",
62-
port.remote_device,
65+
remote_device,
66+
oper_state,
67+
state_duration,
6368
])
6469
click.echo(tabulate(body, header, stralign='right'))
6570

0 commit comments

Comments
 (0)