Skip to content

Commit c217fe5

Browse files
cawandjleveque
authored andcommitted
Connect line implementation (and show line output change) (sonic-net#295)
1 parent 506712e commit c217fe5

File tree

3 files changed

+117
-37
lines changed

3 files changed

+117
-37
lines changed

connect/main.py

+16-15
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import click
44
import errno
55
import os
6+
import pexpect
67
import subprocess
78
import sys
89
from click_default_group import DefaultGroup
@@ -85,18 +86,8 @@ def run_command(command, display_cmd=False):
8586
if display_cmd:
8687
click.echo(click.style("Command: ", fg='cyan') + click.style(command, fg='green'))
8788

88-
proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
89-
90-
while True:
91-
output = proc.stdout.readline()
92-
if output == "" and proc.poll() is not None:
93-
break
94-
elif output:
95-
click.echo(output.rstrip('\n'))
96-
97-
rc = proc.poll()
98-
if rc != 0:
99-
sys.exit(rc)
89+
proc = pexpect.spawn(command)
90+
proc.interact()
10091

10192
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help', '-?'])
10293

@@ -116,9 +107,19 @@ def connect():
116107
@connect.command('line')
117108
@click.argument('linenum')
118109
def line(linenum):
119-
"""Connect to line via serial connection"""
120-
# TODO: Stub
121-
return
110+
"""Connect to line LINENUM via serial connection"""
111+
cmd = "consutil connect " + linenum
112+
run_command(cmd)
113+
114+
#
115+
# 'device' command ("connect device")
116+
#
117+
@connect.command('device')
118+
@click.argument('devicename')
119+
def device(devicename):
120+
"""Connect to device DEVICENAME via serial connection"""
121+
cmd = "consutil connect -d " + devicename
122+
run_command(cmd)
122123

123124
if __name__ == '__main__':
124125
connect()

consutil/lib.py

+56-14
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
try:
99
import click
1010
import re
11+
import swsssdk
1112
import subprocess
1213
import sys
1314
except ImportError as e:
@@ -18,6 +19,22 @@
1819
ERR_CMD = 1
1920
ERR_DEV = 2
2021

22+
CONSOLE_PORT_TABLE = "CONSOLE_PORT"
23+
BAUD_KEY = "baud_rate"
24+
DEVICE_KEY = "remote_device"
25+
FLOW_KEY = "flow_control"
26+
DEFAULT_BAUD = "9600"
27+
28+
# QUIET == True => picocom will not output any messages, and pexpect will wait for console
29+
# switch login or command line to let user interact with shell
30+
# Downside: if console switch output ever does not match DEV_READY_MSG, program will think connection failed
31+
# QUIET == False => picocom will output messages - welcome message is caught by pexpect, so successful
32+
# connection will always lead to user interacting with shell
33+
# Downside: at end of session, picocom will print exit message, exposing picocom to user
34+
QUIET = False
35+
DEV_READY_MSG = r"([Ll]ogin:|[Pp]assword:|[$>#])" # login prompt or command line prompt
36+
TIMEOUT_SEC = 0.2
37+
2138
# runs command, exit if stderr is written to, returns stdout otherwise
2239
# input: cmd (str), output: output of cmd (str)
2340
def run_command(cmd):
@@ -57,8 +74,8 @@ def getBusyDevices():
5774

5875
# matches any number of spaces then any number of digits
5976
regexPid = r" *(\d+)"
60-
# matches anything of form: Xxx Xxx 00 00:00:00 0000
61-
regexDate = r"([A-Z][a-z]{2} [A-Z][a-z]{2} \d{2} \d{2}:\d{2}:\d{2} \d{4})"
77+
# matches anything of form: Xxx Xxx ( 0)or(00) 00:00:00 0000
78+
regexDate = r"([A-Z][a-z]{2} [A-Z][a-z]{2} [\d ]\d \d{2}:\d{2}:\d{2} \d{4})"
6279
# matches any non-whitespace characters ending in minicom or picocom,
6380
# then a space and any chars followed by /dev/ttyUSB<any digits>,
6481
# then a space and any chars
@@ -75,16 +92,41 @@ def getBusyDevices():
7592
busyDevices[linenum_key] = (pid, date)
7693
return busyDevices
7794

78-
# returns baud rate of device corresponding to line number
79-
# input: linenum (str)
80-
def getBaud(linenum):
81-
checkDevice(linenum)
82-
cmd = "sudo stty -F " + DEVICE_PREFIX + str(linenum)
83-
output = run_command(cmd)
95+
# returns actual baud rate, configured baud rate,
96+
# and flow control settings of device corresponding to line number
97+
# input: linenum (str), output: (actual baud (str), configured baud (str), flow control (bool))
98+
def getConnectionInfo(linenum):
99+
config_db = ConfigDBConnector()
100+
config_db.connect()
101+
entry = config_db.get_entry(CONSOLE_PORT_TABLE, str(linenum))
102+
103+
conf_baud = "-" if BAUD_KEY not in entry else entry[BAUD_KEY]
104+
act_baud = DEFAULT_BAUD if conf_baud == "-" else conf_baud
105+
flow_control = False
106+
if FLOW_KEY in entry and entry[FLOW_KEY] == "1":
107+
flow_control = True
108+
109+
return (act_baud, conf_baud, flow_control)
110+
111+
# returns the line number corresponding to target, or exits if line number cannot be found
112+
# if deviceBool, interprets target as device name
113+
# otherwise interprets target as line number
114+
# input: target (str), deviceBool (bool), output: linenum (str)
115+
def getLineNumber(target, deviceBool):
116+
if not deviceBool:
117+
return target
118+
119+
config_db = ConfigDBConnector()
120+
config_db.connect()
84121

85-
match = re.match(r"^speed (\d+) baud;", output)
86-
if match != None:
87-
return match.group(1)
88-
else:
89-
click.echo("Unable to determine baud rate")
90-
return ""
122+
devices = getAllDevices()
123+
linenums = list(map(lambda dev: dev[len(DEVICE_PREFIX):], devices))
124+
125+
for linenum in linenums:
126+
entry = config_db.get_entry(CONSOLE_PORT_TABLE, linenum)
127+
if DEVICE_KEY in entry and entry[DEVICE_KEY] == target:
128+
return linenum
129+
130+
click.echo("Device {} does not exist".format(target))
131+
sys.exit(ERR_DEV)
132+
return ""

consutil/main.py

+45-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
try:
99
import click
10+
import os
11+
import pexpect
1012
import re
1113
import subprocess
1214
from tabulate import tabulate
@@ -15,7 +17,7 @@
1517

1618
@click.group()
1719
def consutil():
18-
"""consutil - Command-line utility for interacting with switchs via console device"""
20+
"""consutil - Command-line utility for interacting with switches via console device"""
1921

2022
if os.geteuid() != 0:
2123
print "Root privileges are required for this operation"
@@ -28,7 +30,7 @@ def show():
2830
devices = getAllDevices()
2931
busyDevices = getBusyDevices()
3032

31-
header = ["Line", "Baud", "PID", "Start Time"]
33+
header = ["Line", "Actual/Configured Baud", "PID", "Start Time"]
3234
body = []
3335
for device in devices:
3436
lineNum = device[11:]
@@ -38,10 +40,13 @@ def show():
3840
if lineNum in busyDevices:
3941
pid, date = busyDevices[lineNum]
4042
busy = "*"
41-
baud = getBaud(lineNum)
43+
actBaud, confBaud, _ = getConnectionInfo(lineNum)
44+
# repeated "~" will be replaced by spaces - hacky way to align the "/"s
45+
baud = "{}/{}{}".format(actBaud, confBaud, "~"*(15-len(confBaud)))
4246
body.append([busy+lineNum, baud, pid, date])
4347

44-
click.echo(tabulate(body, header, stralign="right"))
48+
# replace repeated "~" with spaces - hacky way to align the "/"s
49+
click.echo(tabulate(body, header, stralign="right").replace('~', ' '))
4550

4651
# 'clear' subcommand
4752
@consutil.command()
@@ -62,10 +67,42 @@ def clear(linenum):
6267

6368
# 'connect' subcommand
6469
@consutil.command()
65-
@click.argument('linenum')
66-
def connect(linenum):
67-
"""Connect to switch via console device"""
68-
click.echo("connect linenum")
70+
@click.argument('target')
71+
@click.option('--devicename', '-d', is_flag=True, help="connect by name - if flag is set, interpret linenum as device name instead")
72+
def connect(target, devicename):
73+
"""Connect to switch via console device - TARGET is line number or device name of switch"""
74+
lineNumber = getLineNumber(target, devicename)
75+
checkDevice(lineNumber)
76+
lineNumber = str(lineNumber)
77+
78+
# build and start picocom command
79+
actBaud, _, flowBool = getConnectionInfo(lineNumber)
80+
flowCmd = "h" if flowBool else "n"
81+
quietCmd = "-q" if QUIET else ""
82+
cmd = "sudo picocom -b {} -f {} {} {}{}".format(actBaud, flowCmd, quietCmd, DEVICE_PREFIX, lineNumber)
83+
proc = pexpect.spawn(cmd)
84+
proc.send("\n")
85+
86+
if QUIET:
87+
readyMsg = DEV_READY_MSG
88+
else:
89+
readyMsg = "Terminal ready" # picocom ready message
90+
busyMsg = "Resource temporarily unavailable" # picocom busy message
91+
92+
# interact with picocom or print error message, depending on pexpect output
93+
index = proc.expect([readyMsg, busyMsg, pexpect.EOF, pexpect.TIMEOUT], timeout=TIMEOUT_SEC)
94+
if index == 0: # terminal ready
95+
click.echo("Successful connection to line {}\nPress ^A ^X to disconnect".format(lineNumber))
96+
if QUIET:
97+
# prints picocom output up to and including readyMsg
98+
click.echo(proc.before + proc.match.group(0), nl=False)
99+
proc.interact()
100+
if QUIET:
101+
click.echo("\nTerminating...")
102+
elif index == 1: # resource is busy
103+
click.echo("Cannot connect: line {} is busy".format(lineNumber))
104+
else: # process reached EOF or timed out
105+
click.echo("Cannot connect: unable to open picocom process")
69106

70107
if __name__ == '__main__':
71108
consutil()

0 commit comments

Comments
 (0)