Skip to content

Commit

Permalink
Console show/clear/connect skeleton (sonic-net#278)
Browse files Browse the repository at this point in the history
* new consutil package - includes helper functions, and show/clear/connect skeletons

Signed-off-by: Cayla Wanderman-Milne <t-cawand@microsoft.com>

* Added connect command skeleton, added show line and clear line skeletons

Signed-off-by: Cayla Wanderman-Milne <t-cawand@microsoft.com>

* Added connect and consutil packages to setup.py and to bash completion

Signed-off-by: Cayla Wanderman-Milne <t-cawand@microsoft.com>

* Added docstrings and "TODO Stub" comments to clear line, show line, and connect line

Signed-off-by: Cayla Wanderman-Milne <t-cawand@microsoft.com>
  • Loading branch information
cawand authored and lguohan committed Jul 10, 2018
1 parent 99c997e commit 8f17cdf
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 0 deletions.
11 changes: 11 additions & 0 deletions clear/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,5 +242,16 @@ def clear_vlan_fdb(vlanid):
command = 'fdbclear' + ' -v ' + vlanid
run_command(command)
'''

#
# 'line' command
#
@cli.command('line')
@click.argument('linenum')
def line(linenum):
"""Clear preexisting connection to line"""
# TODO: Stub
return

if __name__ == '__main__':
cli()
Empty file added connect/__init__.py
Empty file.
134 changes: 134 additions & 0 deletions connect/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#! /usr/bin/python -u

import click
import errno
import os
import subprocess
import sys
from click_default_group import DefaultGroup

try:
# noinspection PyPep8Naming
import ConfigParser as configparser
except ImportError:
# noinspection PyUnresolvedReferences
import configparser


# This is from the aliases example:
# https://github.com/pallets/click/blob/57c6f09611fc47ca80db0bd010f05998b3c0aa95/examples/aliases/aliases.py
class Config(object):
"""Object to hold CLI config"""

def __init__(self):
self.path = os.getcwd()
self.aliases = {}

def read_config(self, filename):
parser = configparser.RawConfigParser()
parser.read([filename])
try:
self.aliases.update(parser.items('aliases'))
except configparser.NoSectionError:
pass


# Global Config object
_config = None


# This aliased group has been modified from click examples to inherit from DefaultGroup instead of click.Group.
# DefaultGroup is a superclass of click.Group which calls a default subcommand instead of showing
# a help message if no subcommand is passed
class AliasedGroup(DefaultGroup):
"""This subclass of a DefaultGroup supports looking up aliases in a config
file and with a bit of magic.
"""

def get_command(self, ctx, cmd_name):
global _config

# If we haven't instantiated our global config, do it now and load current config
if _config is None:
_config = Config()

# Load our config file
cfg_file = os.path.join(os.path.dirname(__file__), 'aliases.ini')
_config.read_config(cfg_file)

# Try to get builtin commands as normal
rv = click.Group.get_command(self, ctx, cmd_name)
if rv is not None:
return rv

# No builtin found. Look up an explicit command alias in the config
if cmd_name in _config.aliases:
actual_cmd = _config.aliases[cmd_name]
return click.Group.get_command(self, ctx, actual_cmd)

# Alternative option: if we did not find an explicit alias we
# allow automatic abbreviation of the command. "status" for
# instance will match "st". We only allow that however if
# there is only one command.
matches = [x for x in self.list_commands(ctx)
if x.lower().startswith(cmd_name.lower())]
if not matches:
# No command name matched. Issue Default command.
ctx.arg0 = cmd_name
cmd_name = self.default_cmd_name
return DefaultGroup.get_command(self, ctx, cmd_name)
elif len(matches) == 1:
return DefaultGroup.get_command(self, ctx, matches[0])
ctx.fail('Too many matches: %s' % ', '.join(sorted(matches)))

def run_command(command, display_cmd=False):
if display_cmd:
click.echo(click.style("Command: ", fg='cyan') + click.style(command, fg='green'))

proc = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)

while True:
output = proc.stdout.readline()
if output == "" and proc.poll() is not None:
break
if output:
try:
click.echo(output.rstrip('\n'))
except IOError as e:
# In our version of Click (v6.6), click.echo() and click.echo_via_pager() do not properly handle
# SIGPIPE, and if a pipe is broken before all output is processed (e.g., pipe output to 'head'),
# it will result in a stack trace. This is apparently fixed upstream, but for now, we silently
# ignore SIGPIPE here.
if e.errno == errno.EPIPE:
sys.exit(0)
else:
raise

rc = proc.poll()
if rc != 0:
sys.exit(rc)

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

#
# 'connect' group (root group)
#

# This is our entrypoint - the main "connect" command
@click.group(cls=AliasedGroup, context_settings=CONTEXT_SETTINGS)
def connect():
"""SONiC command line - 'connect' command"""
pass

#
# 'line' command ("connect line")
#
@connect.command('line')
@click.argument('linenum')
def line(linenum):
"""Connect to line via serial connection"""
# TODO: Stub
return

if __name__ == '__main__':
connect()
90 changes: 90 additions & 0 deletions consutil/lib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env python
#
# lib.py
#
# Helper code for CLI for interacting with switches via console device
#

try:
import click
import re
import subprocess
import sys
except ImportError as e:
raise ImportError("%s - required module not found" % str(e))

DEVICE_PREFIX = "/dev/ttyUSB"

ERR_CMD = 1
ERR_DEV = 2

# runs command, exit if stderr is written to, returns stdout otherwise
# input: cmd (str), output: output of cmd (str)
def popenWrapper(cmd):
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
output = proc.stdout.read()
error = proc.stderr.read()
if error != "":
click.echo("Command resulted in error: {}".format(error))
sys.exit(ERR_CMD)
return output

# exits if inputted line number does not correspond to a device
# input: linenum
def checkDevice(linenum):
devices = getAllDevices()
if DEVICE_PREFIX + str(linenum) not in devices:
click.echo("Line number {} does not exist".format(linenum))
sys.exit(ERR_DEV)

# returns a sorted list of all devices (whose name matches DEVICE_PREFIX)
def getAllDevices():
cmd = "ls " + DEVICE_PREFIX + "*"
output = popenWrapper(cmd)

devices = output.split('\n')
devices = list(filter(lambda dev: re.match(DEVICE_PREFIX + r"\d+", dev) != None, devices))
devices.sort(key=lambda dev: int(dev[len(DEVICE_PREFIX):]))

return devices

# returns a dictionary of busy devices and their info
# maps line number to (pid, process start time)
def getBusyDevices():
cmd = 'ps -eo pid,lstart,cmd | grep -E "(mini|pico)com"'
output = popenWrapper(cmd)
processes = output.split('\n')

# matches any number of spaces then any number of digits
regexPid = r" *(\d+)"
# matches anything of form: Xxx Xxx 00 00:00:00 0000
regexDate = r"([A-Z][a-z]{2} [A-Z][a-z]{2} \d{2} \d{2}:\d{2}:\d{2} \d{4})"
# matches any non-whitespace characters ending in minicom or picocom,
# then a space and any chars followed by /dev/ttyUSB<any digits>,
# then a space and any chars
regexCmd = r"\S*(?:(?:mini)|(?:pico))com .*" + DEVICE_PREFIX + r"(\d+)(?: .*)?"
regexProcess = re.compile(r"^"+regexPid+r" "+regexDate+r" "+regexCmd+r"$")

busyDevices = {}
for process in processes:
match = regexProcess.match(process)
if match != None:
pid = match.group(1)
date = match.group(2)
linenum_key = match.group(3)
busyDevices[linenum_key] = (pid, date)
return busyDevices

# returns baud rate of device corresponding to line number
# input: linenum (str)
def getBaud(linenum):
checkDevice(linenum)
cmd = "sudo stty -F " + DEVICE_PREFIX + str(linenum)
output = popenWrapper(cmd)

match = re.match(r"^speed (\d+) baud;", output)
if match != None:
return match.group(1)
else:
click.echo("Unable to determine baud rate")
return ""
44 changes: 44 additions & 0 deletions consutil/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env python
#
# main.py
#
# Command-line utility for interacting with switches over serial via console device
#

try:
import click
import re
import subprocess
except ImportError as e:
raise ImportError("%s - required module not found" % str(e))

@click.group()
def consutil():
"""consutil - Command-line utility for interacting with switchs via console device"""

if os.geteuid() != ""
print "Root privileges are required for this operation"
sys.exit(1)

# 'show' subcommand
@consutil.command()
def line():
"""Show all /dev/ttyUSB lines"""
click.echo("show line")

# 'clear' subcommand
@consutil.command()
@click.argument('linenum')
def clear(linenum):
"""Clear preexisting connection to line"""
click.echo("clear line linenum")

# 'connect' subcommand
@consutil.command()
@click.argument('linenum')
def connect(linenum):
"""Connect to switch via console device"""
click.echo("connect linenum")

if __name__ == '__main__':
consutil()
8 changes: 8 additions & 0 deletions data/etc/bash_completion.d/connect
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
_connect_completion() {
COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \
COMP_CWORD=$COMP_CWORD \
_CONNECT_COMPLETE=complete $1 ) )
return 0
}

complete -F _connect_completion -o default connect;
8 changes: 8 additions & 0 deletions data/etc/bash_completion.d/consutil
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
_consutil_completion() {
COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \
COMP_CWORD=$COMP_CWORD \
_CONSUTIL_COMPLETE=complete $1 ) )
return 0
}

complete -F _consutil_completion -o default consutil;
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ def get_test_suite():
'acl_loader',
'clear',
'config',
'connect',
'consutil',
'counterpoll',
'crm',
'debug',
Expand Down Expand Up @@ -64,6 +66,8 @@ def get_test_suite():
'console_scripts': [
'acl-loader = acl_loader.main:cli',
'config = config.main:cli',
'connect = connect.main:connect',
'consutil = consutil.main:consutil',
'counterpoll = counterpoll.main:cli',
'crm = crm.main:cli',
'debug = debug.main:cli',
Expand Down
11 changes: 11 additions & 0 deletions show/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1091,5 +1091,16 @@ def reboot_cause():
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
click.echo(proc.stdout.read())


#
# 'line' command ("show line")
#
@cli.command('line')
def line():
"""Show all /dev/ttyUSB lines and their info"""
# TODO: Stub
return


if __name__ == '__main__':
cli()

0 comments on commit 8f17cdf

Please sign in to comment.