#!/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

CONSOLE_PORT_TABLE = "CONSOLE_PORT"
BAUD_KEY = "baud_rate"
DEVICE_KEY = "remote_device"
FLOW_KEY = "flow_control"
DEFAULT_BAUD = "9600"

# QUIET == True => picocom will not output any messages, and pexpect will wait for console
#                  switch login or command line to let user interact with shell
#        Downside: if console switch output ever does not match DEV_READY_MSG, program will think connection failed
# QUIET == False => picocom will output messages - welcome message is caught by pexpect, so successful
#                   connection will always lead to user interacting with shell
#         Downside: at end of session, picocom will print exit message, exposing picocom to user
QUIET = False
DEV_READY_MSG = r"([Ll]ogin:|[Pp]assword:|[$>#])" # login prompt or command line prompt
TIMEOUT_SEC = 0.2

# runs command, exit if stderr is written to, returns stdout otherwise
# input: cmd (str), output: output of cmd (str)
def run_command(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

# returns a sorted list of all devices (whose name matches DEVICE_PREFIX)
def getAllDevices():
    cmd = "ls " + DEVICE_PREFIX + "*"
    output = run_command(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

# 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 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 = run_command(cmd)
    processes = output.split('\n')

    # matches any number of spaces then any number of digits
    regexPid = r" *(\d+)"
    # matches anything of form: Xxx Xxx ( 0)or(00) 00:00:00 0000
    regexDate = r"([A-Z][a-z]{2} [A-Z][a-z]{2} [\d ]\d \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 actual baud rate, configured baud rate,
# and flow control settings of device corresponding to line number
# input: linenum (str), output: (actual baud (str), configured baud (str), flow control (bool))
def getConnectionInfo(linenum):
    config_db = ConfigDBConnector()
    config_db.connect()
    entry = config_db.get_entry(CONSOLE_PORT_TABLE, str(linenum))

    conf_baud = "-" if BAUD_KEY not in entry else entry[BAUD_KEY]
    act_baud = DEFAULT_BAUD if conf_baud == "-" else conf_baud
    flow_control = False
    if FLOW_KEY in entry and entry[FLOW_KEY] == "1":
        flow_control = True

    return (act_baud, conf_baud, flow_control)

# returns the line number corresponding to target, or exits if line number cannot be found
# if deviceBool, interprets target as device name
# otherwise interprets target as line number
# input: target (str), deviceBool (bool), output: linenum (str)
def getLineNumber(target, deviceBool):
    if not deviceBool:
        return target

    config_db = ConfigDBConnector()
    config_db.connect()

    devices = getAllDevices()
    linenums = list(map(lambda dev: dev[len(DEVICE_PREFIX):], devices))

    for linenum in linenums:
        entry = config_db.get_entry(CONSOLE_PORT_TABLE, linenum)
        if DEVICE_KEY in entry and entry[DEVICE_KEY] == target:
            return linenum

    click.echo("Device {} does not exist".format(target))
    sys.exit(ERR_DEV)
    return ""

consutil/lib.py
#!/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
    from swsssdk import ConfigDBConnector
except ImportError as e:
    raise ImportError("%s - required module not found" % str(e))

DEVICE_PREFIX = "/dev/ttyUSB"

ERR_CMD = 1
ERR_DEV = 2

CONSOLE_PORT_TABLE = "CONSOLE_PORT"
BAUD_KEY = "baud_rate"
DEVICE_KEY = "remote_device"
FLOW_KEY = "flow_control"
DEFAULT_BAUD = "9600"

# QUIET == True => picocom will not output any messages, and pexpect will wait for console
#                  switch login or command line to let user interact with shell
#        Downside: if console switch output ever does not match DEV_READY_MSG, program will think connection failed
# QUIET == False => picocom will output messages - welcome message is caught by pexpect, so successful
#                   connection will always lead to user interacting with shell
#         Downside: at end of session, picocom will print exit message, exposing picocom to user
QUIET = False
DEV_READY_MSG = r"([Ll]ogin:|[Pp]assword:|[$>#])" # login prompt or command line prompt
TIMEOUT_SEC = 0.2

# runs command, exit if stderr is written to, returns stdout otherwise
# input: cmd (str), output: output of cmd (str)
def run_command(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

# returns a sorted list of all devices (whose name matches DEVICE_PREFIX)
def getAllDevices():
    cmd = "ls " + DEVICE_PREFIX + "*"
    output = run_command(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

# 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 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 = run_command(cmd)
    processes = output.split('\n')

    # matches any number of spaces then any number of digits
    regexPid = r" *(\d+)"
    # matches anything of form: Xxx Xxx ( 0)or(00) 00:00:00 0000
    regexDate = r"([A-Z][a-z]{2} [A-Z][a-z]{2} [\d ]\d \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 actual baud rate, configured baud rate,
# and flow control settings of device corresponding to line number
# input: linenum (str), output: (actual baud (str), configured baud (str), flow control (bool))
def getConnectionInfo(linenum):
    config_db = ConfigDBConnector()
    config_db.connect()
    entry = config_db.get_entry(CONSOLE_PORT_TABLE, str(linenum))

    conf_baud = "-" if BAUD_KEY not in entry else entry[BAUD_KEY]
    act_baud = DEFAULT_BAUD if conf_baud == "-" else conf_baud
    flow_control = False
    if FLOW_KEY in entry and entry[FLOW_KEY] == "1":
        flow_control = True

    return (act_baud, conf_baud, flow_control)

# returns the line number corresponding to target, or exits if line number cannot be found
# if deviceBool, interprets target as device name
# otherwise interprets target as line number
# input: target (str), deviceBool (bool), output: linenum (str)
def getLineNumber(target, deviceBool):
    if not deviceBool:
        return target

    config_db = ConfigDBConnector()
    config_db.connect()

    devices = getAllDevices()
    linenums = list(map(lambda dev: dev[len(DEVICE_PREFIX):], devices))

    for linenum in linenums:
        entry = config_db.get_entry(CONSOLE_PORT_TABLE, linenum)
        if DEVICE_KEY in entry and entry[DEVICE_KEY] == target:
            return linenum

    click.echo("Device {} does not exist".format(target))
    sys.exit(ERR_DEV)
    return ""

consutil/lib.py
diff --git a/consutil/lib.py b/consutil/lib.py
index 0ffa88691d..6dbef913b8 100644
--- a/consutil/lib.py
+++ b/consutil/lib.py
@@ -10,6 +10,7 @@
     import re
     import subprocess
     import sys
+    from swsssdk import ConfigDBConnector
 except ImportError as e:
     raise ImportError("%s - required module not found" % str(e))
 

[consutil] show/connect command failed due to global name 'ConfigDBConnector' is not defined
<!--
If you are reporting a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
repository. If there is a duplicate, please close your issue and add a comment
to the existing issue instead.

If you suspect your issue is a bug, please edit your issue description to
include the BUG REPORT INFORMATION shown below. If you fail to provide this
information within 7 days, we cannot debug your issue and will close it. We
will, however, reopen it if you later provide the information.

For more information about reporting issues, see
https://github.com/Azure/SONiC/wiki#report-issues

---------------------------------------------------
GENERAL SUPPORT INFORMATION
---------------------------------------------------

The GitHub issue tracker is for bug reports and feature requests.
General support can be found at the following locations:

- SONiC Support Forums - https://groups.google.com/forum/#!forum/sonicproject

---------------------------------------------------
BUG REPORT INFORMATION
---------------------------------------------------
Use the commands below to provide key information from your environment:
You do NOT have to include this information if this is a FEATURE REQUEST
-->

**Description**

<!--
Briefly describe the problem you are having in a few paragraphs.
-->

An unexpected error thrown while executing `sudo consutil show` or `sudo consutil connect` command.
```bash
NameError: global name 'ConfigDBConnector' is not defined
```

**Steps to reproduce the issue**
1. Install newest image of SONiC
2. Execute `sudo consutil show` or `sudo consutil connect 1`

**Describe the results you received**
Output:
```bash
admin@sonic:~$ sudo consutil show
Traceback (most recent call last):
  File "/usr/bin/consutil", line 12, in <module>
    sys.exit(consutil())
  File "/usr/lib/python2.7/dist-packages/click/core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "/usr/lib/python2.7/dist-packages/click/core.py", line 717, in main
    rv = self.invoke(ctx)
  File "/usr/lib/python2.7/dist-packages/click/core.py", line 1137, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/lib/python2.7/dist-packages/click/core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/lib/python2.7/dist-packages/click/core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "/usr/lib/python2.7/dist-packages/consutil/main.py", line 43, in show
    actBaud, confBaud, _ = getConnectionInfo(lineNum)
  File "/usr/lib/python2.7/dist-packages/consutil/lib.py", line 98, in getConnectionInfo
    config_db = ConfigDBConnector()
NameError: global name 'ConfigDBConnector' is not defined
admin@sonic:~$ 
admin@sonic:~$ 
admin@sonic:~$ sudo consutil connect 1
Traceback (most recent call last):
  File "/usr/bin/consutil", line 12, in <module>
    sys.exit(consutil())
  File "/usr/lib/python2.7/dist-packages/click/core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "/usr/lib/python2.7/dist-packages/click/core.py", line 717, in main
    rv = self.invoke(ctx)
  File "/usr/lib/python2.7/dist-packages/click/core.py", line 1137, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/usr/lib/python2.7/dist-packages/click/core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/lib/python2.7/dist-packages/click/core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "/usr/lib/python2.7/dist-packages/consutil/main.py", line 79, in connect
    actBaud, _, flowBool = getConnectionInfo(lineNumber)
  File "/usr/lib/python2.7/dist-packages/consutil/lib.py", line 98, in getConnectionInfo
    config_db = ConfigDBConnector()
NameError: global name 'ConfigDBConnector' is not defined
```

**Describe the results you expected**
For `sudo consutil show`, a table with line number/baud/... should be displayed without any error.
For `sudo consutil connect`, a console session should be attached or friendly error message will display if the line is busy.

**Additional information you deem important (e.g. issue happens only occasionally)**
Similar error found in another PR/Issue, maybe current issue and that issue are relating to some recent change? https://github.com/Azure/sonic-utilities/pull/1114

**Output of `show version`**

```
admin@sonic:~$ show version

SONiC Software Version: SONiC.dev_jika_cli01.0-0b4191fe
Distribution: Debian 10.5
Kernel: 4.19.0-9-2-amd64
Build commit: 0b4191fe
Build date: Fri Sep 11 07:36:01 UTC 2020
Built by: jika@jika-vm-01

Platform: x86_64-cel_e1031-r0
HwSKU: Celestica-E1031-T48S4
ASIC: broadcom
Serial Number: R0882B2F160615GD000006
Uptime: 11:41:37 up  1:29,  1 user,  load average: 0.09, 0.67, 1.15

Docker images:
REPOSITORY                    TAG                         IMAGE ID            SIZE
docker-teamd                  dev_jika_cli01.0-0b4191fe   06a9488f9400        390MB
docker-teamd                  latest                      06a9488f9400        390MB
docker-nat                    dev_jika_cli01.0-0b4191fe   9adedebc9007        393MB
docker-nat                    latest                      9adedebc9007        393MB
docker-router-advertiser      dev_jika_cli01.0-0b4191fe   9bca9fa2d027        359MB
docker-router-advertiser      latest                      9bca9fa2d027        359MB
docker-platform-monitor       dev_jika_cli01.0-0b4191fe   11092e178476        433MB
docker-platform-monitor       latest                      11092e178476        433MB
docker-lldp                   dev_jika_cli01.0-0b4191fe   bdc853ee9542        387MB
docker-lldp                   latest                      bdc853ee9542        387MB
docker-dhcp-relay             dev_jika_cli01.0-0b4191fe   787384652391        366MB
docker-dhcp-relay             latest                      787384652391        366MB
docker-database               dev_jika_cli01.0-0b4191fe   08143a381945        359MB
docker-database               latest                      08143a381945        359MB
docker-orchagent              dev_jika_cli01.0-0b4191fe   78b74caafb9b        404MB
docker-orchagent              latest                      78b74caafb9b        404MB
docker-sonic-telemetry        dev_jika_cli01.0-0b4191fe   cd5e957ea52a        429MB
docker-sonic-telemetry        latest                      cd5e957ea52a        429MB
docker-sonic-mgmt-framework   dev_jika_cli01.0-0b4191fe   f91a73bd2111        486MB
docker-sonic-mgmt-framework   latest                      f91a73bd2111        486MB
docker-fpm-frr                dev_jika_cli01.0-0b4191fe   b7a0da93271f        406MB
docker-fpm-frr                latest                      b7a0da93271f        406MB
docker-sflow                  dev_jika_cli01.0-0b4191fe   60511099fae8        394MB
docker-sflow                  latest                      60511099fae8        394MB
docker-snmp                   dev_jika_cli01.0-0b4191fe   c3b585b7970f        399MB
docker-snmp                   latest                      c3b585b7970f        399MB
docker-syncd-brcm             dev_jika_cli01.0-0b4191fe   64ac8a8a6401        447MB
docker-syncd-brcm             latest                      64ac8a8a6401        447MB
```

<!--
Also attach debug file produced by `sudo generate_dump`
-->


