From 06f340c53be8b0ccd447a2ead5a2c123fc4252b8 Mon Sep 17 00:00:00 2001 From: Arvindsrinivasan Lakshmi Narasimhan Date: Wed, 18 Jan 2023 21:18:46 +0000 Subject: [PATCH 1/5] rexec initial commit Signed-off-by: Arvindsrinivasan Lakshmi Narasimhan --- rcli/__init__.py | 0 rcli/get_all_bgp_status.py | 8 + rcli/interactive.py | 76 +++++++++ rcli/linecard.py | 148 ++++++++++++++++++ rcli/main.py | 18 +++ rcli/rexec.py | 54 +++++++ rcli/rshell.py | 44 ++++++ rcli/utils.py | 121 ++++++++++++++ setup.py | 4 + show/update_etchosts.py | 63 ++++++++ sonic-utilities-data/bash_completion.d/rexec | 21 +++ sonic-utilities-data/bash_completion.d/rshell | 21 +++ tests/mock_tables/asic0/state_db.json | 12 ++ tests/remote_cli_test.py | 93 +++++++++++ 14 files changed, 683 insertions(+) create mode 100644 rcli/__init__.py create mode 100644 rcli/get_all_bgp_status.py create mode 100644 rcli/interactive.py create mode 100644 rcli/linecard.py create mode 100644 rcli/main.py create mode 100644 rcli/rexec.py create mode 100644 rcli/rshell.py create mode 100644 rcli/utils.py create mode 100644 show/update_etchosts.py create mode 100644 sonic-utilities-data/bash_completion.d/rexec create mode 100644 sonic-utilities-data/bash_completion.d/rshell create mode 100644 tests/remote_cli_test.py diff --git a/rcli/__init__.py b/rcli/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/rcli/get_all_bgp_status.py b/rcli/get_all_bgp_status.py new file mode 100644 index 0000000000..8a72b5044c --- /dev/null +++ b/rcli/get_all_bgp_status.py @@ -0,0 +1,8 @@ +from click.testing import CliRunner +from rcli import rexec + +runner = CliRunner() + +result = runner.invoke(rexec.cli, ["all", "-c", "show ip bgp summary", "-p","password.txt"]) + +print(result.output.strip("\n")) \ No newline at end of file diff --git a/rcli/interactive.py b/rcli/interactive.py new file mode 100644 index 0000000000..85a91aec94 --- /dev/null +++ b/rcli/interactive.py @@ -0,0 +1,76 @@ +# From https://github.com/paramiko/paramiko/blob/main/demos/interactive.py +# +####################################################################### +# +# Copyright (C) 2003-2007 Robey Pointer +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import select +import socket +import sys +import termios +import tty + +from paramiko.py3compat import u +from paramiko import Channel + + +def interactive_shell(channel: Channel): + """ + Continuously wait for commands and execute them + + The function is a loop that waits for input from either the channel or the terminal. If input is + received from the channel, it is printed to the terminal. If input is received from the terminal, it + is sent to the channel. + + :param channel: The channel object we use to communicate with the linecard + :type channel: paramiko.Channel + """ + # Save the current tty so we can return to it later + oldtty = termios.tcgetattr(sys.stdin) + try: + tty.setraw(sys.stdin.fileno()) + tty.setcbreak(sys.stdin.fileno()) + channel.settimeout(0.0) + + while True: + # Continuously wait for commands and execute them + r, w, e = select.select([channel, sys.stdin], [], []) + if channel in r: + try: + # Get output from channel + x = u(channel.recv(1024)) + if len(x) == 0: + # logout message will be displayed + break + # Write channel output to terminal + sys.stdout.write(x) + sys.stdout.flush() + except socket.timeout: + pass + if sys.stdin in r: + # If we are able to send input, get the input from stdin + x = sys.stdin.read(1) + if len(x) == 0: + break + # Send the input to the channel + channel.send(x) + + finally: + # Now that the channel has been exited, return to the previously-saved old tty + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) \ No newline at end of file diff --git a/rcli/linecard.py b/rcli/linecard.py new file mode 100644 index 0000000000..83c36002e9 --- /dev/null +++ b/rcli/linecard.py @@ -0,0 +1,148 @@ +import click +import os +import paramiko + +from getpass import getpass +from .utils import get_linecard_ip, get_password +from . import interactive + +EMPTY_OUTPUTS = ['', '\x1b[?2004l\r'] + +class Linecard: + + def __init__(self, linecard_name, username, password=None, use_ssh_keys=False): + """ + Initialize Linecard object and store credentials, connection, and channel + + :param linecard_name: The name of the linecard you want to connect to + :param username: The username to use to connect to the linecard + :param password: The linecard password. If password not provided, it + will prompt the user for it + :param use_ssh_keys: Whether or not to use SSH keys to authenticate. + """ + self.ip = get_linecard_ip(linecard_name) + + if not self.ip: + click.echo("Linecard '{}' not found.\n".format(linecard_name)) + self.connection = None + return None + + self.linecard_name = linecard_name + self.username = username + + if use_ssh_keys and os.environ.get("SSH_AUTH_SOCK"): + # The user wants to use SSH keys and the ssh agent is running + self.connection = paramiko.SSHClient() + # if ip address not in known_hosts, ignore known_hosts error + self.connection.load_system_host_keys() + self.connection.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + ssh_agent = paramiko.Agent() + available_keys = ssh_agent.get_keys() + if available_keys: + # Try to connect using all keys + connected = False + for key in available_keys: + try: + self.connection.connect(self.ip, username=username, pkey=key) + # If we connected successfully without error, break out of loop + connected = True + break + except paramiko.ssh_exception.AuthenticationException: + # key didn't work + continue + if not connected: + # None of the available keys worked, copy new keys over + password = password if password is not None else get_password(username) + self.ssh_copy_id(password) + else: + # host does not trust this client, perform ssh-copy-id + password = password if password is not None else get_password(username) + self.ssh_copy_id(password) + + else: + password = password if password is not None else getpass( + "Password for username '{}': ".format(username), + # Pass in click stdout stream - this is similar to using click.echo + stream=click.get_text_stream('stdout') + ) + self.connection = paramiko.SSHClient() + # if ip address not in known_hosts, ignore known_hosts error + self.connection.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + self.connection.connect(self.ip, username=self.username, password=password) + except paramiko.ssh_exception.NoValidConnectionsError as e: + self.connection = None + click.echo(e) + + def ssh_copy_id(self, password:str) -> None: + """ + This function generates a new ssh key, copies it to the remote server, + and adds it to the ssh-agent for 15 minutes + + :param password: The password for the user + :type password: str + """ + default_key_path = os.path.expanduser(os.path.join("~/",".ssh","id_rsa")) + # If ssh keys don't exist, create them + if not os.path.exists(default_key_path): + os.system( + 'ssh-keygen -f {} -N "" > /dev/null'.format(default_key_path) + ) + + # Get contents of public keys + pub_key = open(default_key_path + ".pub", "rt") + pub_key_contents = pub_key.read() + pub_key.close() + + # Connect to linecard using password + self.connection.connect(self.ip, username=self.username, password=password) + + # Create ssh directory (if it doesn't exist) and add supervisor public + # key to authorized_keys + self.connection.exec_command('mkdir ~/.ssh -p \n') + self.connection.exec_command( + 'echo \'{}\' >> ~/.ssh/authorized_keys \n' + .format(pub_key_contents) + ) + + # Add key to supervisor SSH Agent with 15 minute timeout + os.system('ssh-add -t 15m {}'.format(default_key_path)) + + # Now that keys are stored in SSH Agent, remove keys from disk + os.remove(default_key_path) + os.remove('{}.pub'.format(default_key_path)) + + def start_shell(self) -> None: + """ + Opens a session, gets a pseudo-terminal, invokes a shell, and then + attaches the host shell to the remote shell. + """ + # Create shell session + self.channel = self.connection.get_transport().open_session() + self.channel.get_pty() + self.channel.invoke_shell() + # Use Paramiko Interactive script to connect to the shell + interactive.interactive_shell(self.channel) + # After user exits interactive shell, close the connection + self.connection.close() + + + def execute_cmd(self, command) -> str: + """ + Takes a command as an argument, executes it on the remote shell, and returns the output + + :param command: The command to execute on the remote shell + :return: The output of the command. + """ + # Execute the command and gather errors and output + _, stdout, stderr = self.connection.exec_command(command + "\n") + output = stdout.read().decode('utf-8') + + if stderr: + # Error was present, add message to output + output += stderr.read().decode('utf-8') + + # Close connection and return output + self.connection.close() + return output diff --git a/rcli/main.py b/rcli/main.py new file mode 100644 index 0000000000..53d8157b98 --- /dev/null +++ b/rcli/main.py @@ -0,0 +1,18 @@ +import click + + +# +# 'rcli' group (root group) +# + +# This is our entrypoint - the main "show" command +@click.command() +# @click.pass_context +def cli(): + """ + SONiC command line - 'rcli' command. + + Usage: rexec LINECARDS -c \"COMMAND\" + or rshell LINECARD + """ + print(cli.__doc__) \ No newline at end of file diff --git a/rcli/rexec.py b/rcli/rexec.py new file mode 100644 index 0000000000..5024593182 --- /dev/null +++ b/rcli/rexec.py @@ -0,0 +1,54 @@ +import os +import click +import paramiko + +from .linecard import Linecard +from .utils import get_all_linecards, get_password, get_password_from_file + +@click.command() +@click.argument('linecard_names', nargs=-1, type=str, required=True, autocompletion=get_all_linecards) +@click.option('-c', '--command', type=str, required=True) +@click.option('-k','--use-ssh-keys/--no-keys', default=False) +@click.option('-p','--password-filename', type=str) +def cli(linecard_names, command, use_ssh_keys=False, password_filename=None): + """ + Executes a command on one or many linecards + + :param linecard_names: A list of linecard names to execute the command on, + use `all` to execute on all linecards. + :param command: The command to execute on the linecard(s) + :param use_ssh_keys: If True, will attempt to use ssh keys to login to the + linecard. If False, will prompt for password, defaults to False (optional) + :param password_filename: A file containing the password for the linecard. If + not provided inline, user will be prompted for password. File should be + relative to current path. + """ + username = os.getlogin() + + if list(linecard_names) == ["all"]: + # Get all linecard names using autocompletion helper + linecard_names = get_all_linecards(None, None, "") + + if use_ssh_keys: + # If we want to use ssh keys, check if the user provided a password + password = None if not password_filename else get_password_from_file(password_filename) + elif password_filename: + # Don't use ssh keys and read password from file + password = get_password_from_file(password_filename) + else: + # Password filename was not provided, read password from user input + password = get_password(username) + + # Iterate through each linecard, execute command, and gather output + for linecard_name in linecard_names: + try: + lc = Linecard(linecard_name, username, password, use_ssh_keys) + if lc.connection: + # If connection was created, connection exists. Otherwise, user will see an error message. + click.echo("======== {} output: ========".format(lc.linecard_name)) + click.echo(lc.execute_cmd(command)) + except paramiko.ssh_exception.AuthenticationException: + click.echo("Login failed on '{}' with username '{}'".format(linecard_name, username)) + +if __name__=="__main__": + cli(prog_name='rexec') diff --git a/rcli/rshell.py b/rcli/rshell.py new file mode 100644 index 0000000000..0fd137c1d1 --- /dev/null +++ b/rcli/rshell.py @@ -0,0 +1,44 @@ +import os +import click +import paramiko + +from .linecard import Linecard +from .utils import get_all_linecards, get_password, get_password_from_file + +@click.command() +@click.argument('linecard_name', type=str, autocompletion=get_all_linecards) +@click.option('-k','--use-ssh-keys/--no-keys', default=False) +@click.option('-p','--password-filename', type=str) +def cli(linecard_name, use_ssh_keys=False,password_filename=None): + """ + Open interactive shell for one linecard + + :param linecard_name: The name of the linecard to connect to + :param use_ssh_keys: If True, will attempt to use ssh keys to login to the + linecard. If False, will prompt for password, defaults to False (optional) + :param password_filename: The password for the linecard, if not provided inline, + user will be prompted for password + """ + username = os.getlogin() + + if use_ssh_keys: + # If we want to use ssh keys, check if the user provided a password + password = None if not password_filename else get_password_from_file(password_filename) + elif password_filename: + # Don't use ssh keys and read password from file + password = get_password_from_file(password_filename) + else: + # Password filename was not provided, read password from user input + password = get_password(username) + + try: + lc = Linecard(linecard_name, username, password, use_ssh_keys) + if lc.connection: + # If connection was created, connection exists. Otherwise, user will see an error message. + lc.start_shell() + except paramiko.ssh_exception.AuthenticationException: + click.echo("Login failed on '{}' with username '{}'".format(linecard_name, username)) + + +if __name__=="__main__": + cli(prog_name='rshell') diff --git a/rcli/utils.py b/rcli/utils.py new file mode 100644 index 0000000000..0518c95d1a --- /dev/null +++ b/rcli/utils.py @@ -0,0 +1,121 @@ +import click +from getpass import getpass +import os + +from swsscommon.swsscommon import SonicV2Connector + +CHASSIS_MODULE_INFO_TABLE = 'CHASSIS_MODULE_TABLE' +CHASSIS_MODULE_INFO_KEY_TEMPLATE = 'CHASSIS_MODULE {}' +CHASSIS_MODULE_INFO_DESC_FIELD = 'desc' +CHASSIS_MODULE_INFO_SLOT_FIELD = 'slot' +CHASSIS_MODULE_INFO_OPERSTATUS_FIELD = 'oper_status' +CHASSIS_MODULE_INFO_ADMINSTATUS_FIELD = 'admin_status' + +CHASSIS_MIDPLANE_INFO_TABLE = 'CHASSIS_MIDPLANE_TABLE' +CHASSIS_MIDPLANE_INFO_IP_FIELD = 'ip_address' +CHASSIS_MIDPLANE_INFO_ACCESS_FIELD = 'access' + +def get_linecard_ip(linecard_name: str): + """ + Given a linecard name, lookup its IP address in the midplane table + + :param linecard_name: The name of the linecard you want to connect to + :type linecard_name: str + :return: IP address of the linecard + """ + # Adapted from `show chassis modules midplane-status` command logic: + # https://github.com/sonic-net/sonic-utilities/blob/master/show/chassis_modules.py + + state_db = SonicV2Connector(host="127.0.0.1") + state_db.connect(state_db.STATE_DB) + + key_pattern = '*' + + keys = state_db.keys(state_db.STATE_DB, CHASSIS_MIDPLANE_INFO_TABLE + key_pattern) + if not keys: + click.echo('{} table is empty'.format(CHASSIS_MIDPLANE_INFO_TABLE)) + return None + + for key in keys: + key_list = key.split('|') + if len(key_list) != 2: # error data in DB, log it and ignore + click.echo('Warn: Invalid Key {} in {} table'.format(key, CHASSIS_MIDPLANE_INFO_TABLE)) + continue + + data_dict = state_db.get_all(state_db.STATE_DB, key) + ip = data_dict[CHASSIS_MIDPLANE_INFO_IP_FIELD] + + if key_list[1].lower().replace("-","") == linecard_name.lower().replace("-",""): + return ip + + # Not able to find linecard in table + return None + +def get_all_linecards(ctx, args, incomplete) -> list: + """ + Return a list of all accessible linecard names. This function is used to + autocomplete linecard names in the CLI. + + :param ctx: The Click context object that is passed to the command function + :param args: The arguments passed to the Click command + :param incomplete: The string that the user has typed so far + :return: A list of all accessible linecard names. + """ + # Adapted from `show chassis modules midplane-status` command logic: + # https://github.com/sonic-net/sonic-utilities/blob/master/show/chassis_modules.py + + state_db = SonicV2Connector(host="127.0.0.1") + state_db.connect(state_db.STATE_DB) + + key_pattern = '*' + + keys = state_db.keys(state_db.STATE_DB, CHASSIS_MIDPLANE_INFO_TABLE + key_pattern) + if not keys: + click.echo('{} table is empty'.format(CHASSIS_MIDPLANE_INFO_TABLE)) + return [] + + linecards = [] + + for key in keys: + key_list = key.split('|') + if len(key_list) != 2: # error data in DB, log it and ignore + click.echo('Warn: Invalid Key {} in {} table'.format(key, CHASSIS_MIDPLANE_INFO_TABLE)) + continue + + data_dict = state_db.get_all(state_db.STATE_DB, key) + linecard_name = key_list[1].lower().replace("-","") + linecard_ip = data_dict[CHASSIS_MIDPLANE_INFO_IP_FIELD] + access = data_dict[CHASSIS_MIDPLANE_INFO_ACCESS_FIELD] + + if access == "True": + linecards.append(linecard_name) + + # Return a list of all matched linecards + return [lc for lc in linecards if incomplete in lc] + +def get_password_from_file(password_filename: str) -> str: + """ + Read the password from the file and return + + :param password_filename: The path to the file containing the password, if + not provided the user will be prompted for password + :type password_filename: str + :return: The password for the username + """ + with open(os.path.expanduser(password_filename), "r") as file: + return file.read().replace("\n","") + +def get_password(username: str) -> str: + """ + Prompts the user for a password, and returns the password + + :param username: The username that we want to get the password for + :type username: str + :return: The password for the username. + """ + return "123456" + return getpass( + "Password for username '{}': ".format(username), + # Pass in click stdout stream - this is similar to using click.echo + stream=click.get_text_stream('stdout') + ) \ No newline at end of file diff --git a/setup.py b/setup.py index 70d7473bd7..fe89026d86 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,7 @@ 'pddf_thermalutil', 'pddf_ledutil', 'syslog_util', + 'rcli', 'show', 'show.interfaces', 'show.plugins', @@ -179,6 +180,8 @@ 'pddf_psuutil = pddf_psuutil.main:cli', 'pddf_thermalutil = pddf_thermalutil.main:cli', 'pddf_ledutil = pddf_ledutil.main:cli', + 'rexec = rcli.rexec:cli', + 'rshell = rcli.rshell:cli', 'show = show.main:cli', 'sonic-clear = clear.main:cli', 'sonic-installer = sonic_installer.main:sonic_installer', @@ -206,6 +209,7 @@ 'natsort>=6.2.1', # 6.2.1 is the last version which supports Python 2. Can update once we no longer support Python 2 'netaddr>=0.8.0', 'netifaces>=0.10.7', + 'paramiko>=2.11.0', 'pexpect>=4.8.0', 'semantic-version>=2.8.5', 'prettyprinter>=0.18.0', diff --git a/show/update_etchosts.py b/show/update_etchosts.py new file mode 100644 index 0000000000..8f13fe8df1 --- /dev/null +++ b/show/update_etchosts.py @@ -0,0 +1,63 @@ +import click + +import utilities_common.cli as clicommon +from swsscommon.swsscommon import SonicV2Connector + +from natsort import natsorted +from tabulate import tabulate + +CHASSIS_MODULE_INFO_TABLE = 'CHASSIS_MODULE_TABLE' +CHASSIS_MODULE_INFO_KEY_TEMPLATE = 'CHASSIS_MODULE {}' +CHASSIS_MODULE_INFO_DESC_FIELD = 'desc' +CHASSIS_MODULE_INFO_SLOT_FIELD = 'slot' +CHASSIS_MODULE_INFO_OPERSTATUS_FIELD = 'oper_status' +CHASSIS_MODULE_INFO_ADMINSTATUS_FIELD = 'admin_status' + +CHASSIS_MIDPLANE_INFO_TABLE = 'CHASSIS_MIDPLANE_TABLE' +CHASSIS_MIDPLANE_INFO_IP_FIELD = 'ip_address' +CHASSIS_MIDPLANE_INFO_ACCESS_FIELD = 'access' + +@click.command() +@clicommon.pass_db +@click.argument('chassis_module_name', metavar='', required=False) +def update(db, chassis_module_name): + """Show chassis-modules status""" + + header = ['Name', 'Description', 'Physical-Slot', 'Oper-Status', 'Admin-Status'] + chassis_cfg_table = db.cfgdb.get_table('CHASSIS_MODULE') + + state_db = SonicV2Connector(host="127.0.0.1") + state_db.connect(state_db.STATE_DB) + + key_pattern = '*' + if chassis_module_name: + key_pattern = '|' + chassis_module_name + + keys = state_db.keys(state_db.STATE_DB, CHASSIS_MODULE_INFO_TABLE + key_pattern) + if not keys: + print('Key {} not found in {} table'.format(key_pattern, CHASSIS_MODULE_INFO_TABLE)) + return + + table = [] + for key in natsorted(keys): + key_list = key.split('|') + if len(key_list) != 2: # error data in DB, log it and ignore + print('Warn: Invalid Key {} in {} table'.format(key, CHASSIS_MODULE_INFO_TABLE)) + continue + + data_dict = state_db.get_all(state_db.STATE_DB, key) + desc = data_dict[CHASSIS_MODULE_INFO_DESC_FIELD] + slot = data_dict[CHASSIS_MODULE_INFO_SLOT_FIELD] + oper_status = data_dict[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] + + admin_status = 'up' + config_data = chassis_cfg_table.get(key_list[1]) + if config_data is not None: + admin_status = config_data.get(CHASSIS_MODULE_INFO_ADMINSTATUS_FIELD) + + table.append((key_list[1], desc, slot, oper_status, admin_status)) + + if table: + click.echo(tabulate(table, header, tablefmt='simple', stralign='right')) + else: + click.echo('No data available in CHASSIS_MODULE_TABLE\n') \ No newline at end of file diff --git a/sonic-utilities-data/bash_completion.d/rexec b/sonic-utilities-data/bash_completion.d/rexec new file mode 100644 index 0000000000..1199fd0676 --- /dev/null +++ b/sonic-utilities-data/bash_completion.d/rexec @@ -0,0 +1,21 @@ +_rexec_completion() { + local IFS=$' +' + COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \ + COMP_CWORD=$COMP_CWORD \ + _REXEC_COMPLETE=complete $1 ) ) + return 0 +} + +_rexec_completionetup() { + local COMPLETION_OPTIONS="" + local BASH_VERSION_ARR=(${BASH_VERSION//./ }) + # Only BASH version 4.4 and later have the nosort option. + if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] && [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then + COMPLETION_OPTIONS="-o nosort" + fi + + complete $COMPLETION_OPTIONS -F _rexec_completion rexec +} + +_rexec_completionetup; \ No newline at end of file diff --git a/sonic-utilities-data/bash_completion.d/rshell b/sonic-utilities-data/bash_completion.d/rshell new file mode 100644 index 0000000000..012f754dd7 --- /dev/null +++ b/sonic-utilities-data/bash_completion.d/rshell @@ -0,0 +1,21 @@ +_rshell_completion() { + local IFS=$' +' + COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \ + COMP_CWORD=$COMP_CWORD \ + _RSHELL_COMPLETE=complete $1 ) ) + return 0 +} + +_rshell_completionetup() { + local COMPLETION_OPTIONS="" + local BASH_VERSION_ARR=(${BASH_VERSION//./ }) + # Only BASH version 4.4 and later have the nosort option. + if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] && [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then + COMPLETION_OPTIONS="-o nosort" + fi + + complete $COMPLETION_OPTIONS -F _rshell_completion rshell +} + +_rshell_completionetup; \ No newline at end of file diff --git a/tests/mock_tables/asic0/state_db.json b/tests/mock_tables/asic0/state_db.json index 21b4fa0eab..be7baf9743 100644 --- a/tests/mock_tables/asic0/state_db.json +++ b/tests/mock_tables/asic0/state_db.json @@ -249,5 +249,17 @@ "STATUS": "up", "REMOTE_MOD": "0", "REMOTE_PORT": "93" + }, + "CHASSIS_MIDPLANE_TABLE|LINE-CARD0": { + "ip_address": "127.0.0.1", + "access": "True" + }, + "CHASSIS_MIDPLANE_TABLE|LINE-CARD1": { + "ip_address": "127.0.0.1", + "access": "True" + }, + "CHASSIS_MIDPLANE_TABLE|LINE-CARD2": { + "ip_address": "127.0.0.1", + "access": "False" } } diff --git a/tests/remote_cli_test.py b/tests/remote_cli_test.py new file mode 100644 index 0000000000..d545540740 --- /dev/null +++ b/tests/remote_cli_test.py @@ -0,0 +1,93 @@ +# import os +# from unittest import mock +# from click.testing import CliRunner +# from swsscommon.swsscommon import SonicV2Connector +# from .mock_tables import dbconnector +# from utilities_common.db import Db + +# import show.main as show +# import rcli +# from rcli import rexec +# from rcli import rshell + +# test_path = os.path.dirname(os.path.abspath(__file__)) +# mock_db_path = os.path.join(test_path, "remote_cli") +# header_lines = 2 + + +# class TestRemoteCLI(object): +# @classmethod +# def setup_class(cls): +# print("SETUP") +# os.environ["UTILITIES_UNIT_TESTING"] = "1" + +# def set_db_values(self, db, key, kvs): +# for field, value in kvs.items(): +# db.set(db.STATE_DB, key, field, value) + +# # @mock.patch("rcli.utils.get_linecard_ip", mock.MagicMock(return_value="1.1.1.1")) +# @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) +# @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="123456")) +# def test_rexec_echo(self): +# runner = CliRunner() +# db = Db() +# dbconnector = db.db +# LINECARD_NAME = "LINE-CARD0" + +# runner = CliRunner() +# result = runner.invoke(show.cli.commands["chassis"].commands["modules"].commands["midplane-status"], obj=db) +# result_lines = result.output.strip('\n').split('\n') +# assert result.exit_code == 0 +# result_out = (result_lines[header_lines]).split() +# print(result_out) +# assert result_out[2] == 'True', "Unable to find LINE-CARD0 in CHASSIS_MIDPLANE_TABLE" + +# linecard_instance = mock.MagicMock() +# linecard_instance.connection = True +# linecard_instance.linecard_name = LINECARD_NAME +# linecard_instance.execute_cmd.return_value = "hello world\n" + + +# rcli.utils.get_linecard_ip = mock.MagicMock(return_value="1.1.1.1") + + +# print(dir(rcli.utils)) + +# result = runner.invoke(rexec.cli, [LINECARD_NAME, "-c", "pwd"], obj=db) +# print(result.output) +# assert result.exit_code == 0, result.output +# assert "hello world" in result.output + +# # with runner.isolated_filesystem(): +# # with open('password.txt', 'w') as f: +# # f.write('123456') +# # result = runner.invoke(rexec.cli, [LINECARD_NAME, "-c", "pwd"]) +# # print(result) +# # assert result.exit_code == 0, result.output +# # assert "hello world" in result.output + +# # @mock.patch("paramiko") +# # def test_rexec_echo(self, paramiko): +# # runner = CliRunner() +# # db = Db() +# # dbconnector = db.db +# # LINECARD_NAME = "LINE-CARD0" + +# # runner = CliRunner() +# # result = runner.invoke(show.cli.commands["chassis"].commands["modules"].commands["midplane-status"], [LINECARD_NAME], obj=db) +# # result_lines = result.output.strip('\n').split('\n') +# # assert result.exit_code == 0 +# # result_out = (result_lines[header_lines]).split() +# # assert result_out[2] == 'True', "Unable to find LINE-CARD0 in CHASSIS_MIDPLANE_TABLE" + +# # ssh_client_instance = mock.MagicMock() +# # ssh_client_instance.connection = mock.MagicMock() + +# # must mock for all test cases +# # ssh_client_instance.connection.exec_command.return_value + +# # ssh-copy-id +# # mock_os = mock.MagicMock() +# # mock_os.exists.return_value = True + + \ No newline at end of file From 5426dfbc6ca2369539fb40e77fa9f2487df8bd86 Mon Sep 17 00:00:00 2001 From: Arvindsrinivasan Lakshmi Narasimhan Date: Tue, 28 Feb 2023 20:30:59 +0000 Subject: [PATCH 2/5] rcli ut changes Signed-off-by: Arvindsrinivasan Lakshmi Narasimhan --- rcli/linecard.py | 196 +++++++------- rcli/main.py | 28 +- rcli/rexec.py | 36 +-- rcli/rshell.py | 38 ++- rcli/utils.py | 136 ++++++---- tests/chassis_modules_test.py | 12 +- tests/mock_tables/chassis_state_db.json | 9 + tests/mock_tables/database_config.json | 5 + tests/remote_cli_test.py | 345 +++++++++++++++++------- 9 files changed, 498 insertions(+), 307 deletions(-) create mode 100644 tests/mock_tables/chassis_state_db.json diff --git a/rcli/linecard.py b/rcli/linecard.py index 83c36002e9..8a0f2d0059 100644 --- a/rcli/linecard.py +++ b/rcli/linecard.py @@ -1,16 +1,22 @@ import click import os import paramiko +import sys +import select +import socket +import sys +import termios +import tty -from getpass import getpass -from .utils import get_linecard_ip, get_password -from . import interactive +from .utils import get_linecard_ip +from paramiko.py3compat import u +from paramiko import Channel EMPTY_OUTPUTS = ['', '\x1b[?2004l\r'] class Linecard: - def __init__(self, linecard_name, username, password=None, use_ssh_keys=False): + def __init__(self, linecard_name, username, password): """ Initialize Linecard object and store credentials, connection, and channel @@ -23,109 +29,107 @@ def __init__(self, linecard_name, username, password=None, use_ssh_keys=False): self.ip = get_linecard_ip(linecard_name) if not self.ip: - click.echo("Linecard '{}' not found.\n".format(linecard_name)) - self.connection = None - return None + sys.exit(1) self.linecard_name = linecard_name self.username = username + self.password = password - if use_ssh_keys and os.environ.get("SSH_AUTH_SOCK"): - # The user wants to use SSH keys and the ssh agent is running - self.connection = paramiko.SSHClient() - # if ip address not in known_hosts, ignore known_hosts error - self.connection.load_system_host_keys() - self.connection.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - ssh_agent = paramiko.Agent() - available_keys = ssh_agent.get_keys() - if available_keys: - # Try to connect using all keys - connected = False - for key in available_keys: - try: - self.connection.connect(self.ip, username=username, pkey=key) - # If we connected successfully without error, break out of loop - connected = True - break - except paramiko.ssh_exception.AuthenticationException: - # key didn't work - continue - if not connected: - # None of the available keys worked, copy new keys over - password = password if password is not None else get_password(username) - self.ssh_copy_id(password) - else: - # host does not trust this client, perform ssh-copy-id - password = password if password is not None else get_password(username) - self.ssh_copy_id(password) - - else: - password = password if password is not None else getpass( - "Password for username '{}': ".format(username), - # Pass in click stdout stream - this is similar to using click.echo - stream=click.get_text_stream('stdout') - ) - self.connection = paramiko.SSHClient() - # if ip address not in known_hosts, ignore known_hosts error - self.connection.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - try: - self.connection.connect(self.ip, username=self.username, password=password) - except paramiko.ssh_exception.NoValidConnectionsError as e: - self.connection = None - click.echo(e) - - def ssh_copy_id(self, password:str) -> None: + self.connection = self._connect() + + + def _connect(self): + connection = paramiko.SSHClient() + # if ip address not in known_hosts, ignore known_hosts error + connection.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + connection.connect(self.ip, username=self.username, password=self.password) + except paramiko.ssh_exception.NoValidConnectionsError as e: + connection = None + click.echo(e) + return connection + + def _get_password(self): """ - This function generates a new ssh key, copies it to the remote server, - and adds it to the ssh-agent for 15 minutes + Prompts the user for a password, and returns the password - :param password: The password for the user - :type password: str + :param username: The username that we want to get the password for + :type username: str + :return: The password for the username. """ - default_key_path = os.path.expanduser(os.path.join("~/",".ssh","id_rsa")) - # If ssh keys don't exist, create them - if not os.path.exists(default_key_path): - os.system( - 'ssh-keygen -f {} -N "" > /dev/null'.format(default_key_path) - ) - - # Get contents of public keys - pub_key = open(default_key_path + ".pub", "rt") - pub_key_contents = pub_key.read() - pub_key.close() - - # Connect to linecard using password - self.connection.connect(self.ip, username=self.username, password=password) - - # Create ssh directory (if it doesn't exist) and add supervisor public - # key to authorized_keys - self.connection.exec_command('mkdir ~/.ssh -p \n') - self.connection.exec_command( - 'echo \'{}\' >> ~/.ssh/authorized_keys \n' - .format(pub_key_contents) - ) - # Add key to supervisor SSH Agent with 15 minute timeout - os.system('ssh-add -t 15m {}'.format(default_key_path)) + return getpass( + "Password for username '{}': ".format(self.username), + # Pass in click stdout stream - this is similar to using click.echo + stream=click.get_text_stream('stdout') + ) - # Now that keys are stored in SSH Agent, remove keys from disk - os.remove(default_key_path) - os.remove('{}.pub'.format(default_key_path)) + def _set_tty_params(self): + tty.setraw(sys.stdin.fileno()) + tty.setcbreak(sys.stdin.fileno()) + + def _is_data_to_read(self, read): + if self.channel in read: + return True + return False + + def _is_data_to_write(self, read): + if sys.stdin in read: + return True + return False + + def _write_to_terminal(self, data): + # Write channel output to terminal + sys.stdout.write(data.decode()) + sys.stdout.flush() + + def _start_interactive_shell(self): + # #import pdb; pdb.set_trace() + # oldtty = termios.tcgetattr(sys.stdin) + try: + self._set_tty_params() + self.channel.settimeout(0.0) + + while True: + #Continuously wait for commands and execute them + read, write, ex = select.select([self.channel, sys.stdin], [], []) + if self._is_data_to_read(read): + try: + # Get output from channel + x = u(self.channel.recv(1024)) + if len(x) == 0: + # logout message will be displayed + break + self._write_to_terminal(x) + except socket.timeout as e: + click.echo("Connection timed out") + break + if self._is_data_to_write(read): + # If we are able to send input, get the input from stdin + x = sys.stdin.read(1) + if len(x) == 0: + break + # Send the input to the channel + self.channel.send(x) + finally: + # Now that the channel has been exited, return to the previously-saved old tty + #termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) + pass + def start_shell(self) -> None: - """ - Opens a session, gets a pseudo-terminal, invokes a shell, and then - attaches the host shell to the remote shell. - """ - # Create shell session - self.channel = self.connection.get_transport().open_session() - self.channel.get_pty() - self.channel.invoke_shell() - # Use Paramiko Interactive script to connect to the shell - interactive.interactive_shell(self.channel) - # After user exits interactive shell, close the connection - self.connection.close() + """ + Opens a session, gets a pseudo-terminal, invokes a shell, and then + attaches the host shell to the remote shell. + """ + # Create shell session + self.channel = self.connection.get_transport().open_session() + self.channel.get_pty() + self.channel.invoke_shell() + # Use Paramiko Interactive script to connect to the shell + self._start_interactive_shell() + # After user exits interactive shell, close the connection + self.connection.close() def execute_cmd(self, command) -> str: diff --git a/rcli/main.py b/rcli/main.py index 53d8157b98..b8c04e7036 100644 --- a/rcli/main.py +++ b/rcli/main.py @@ -1,18 +1,18 @@ -import click +# import click -# -# 'rcli' group (root group) -# +# # +# # 'rcli' group (root group) +# # -# This is our entrypoint - the main "show" command -@click.command() -# @click.pass_context -def cli(): - """ - SONiC command line - 'rcli' command. +# # This is our entrypoint - the main "show" command +# @click.command() +# # @click.pass_context +# def cli(): +# """ +# SONiC command line - 'rcli' command. - Usage: rexec LINECARDS -c \"COMMAND\" - or rshell LINECARD - """ - print(cli.__doc__) \ No newline at end of file +# Usage: rexec LINECARDS -c \"COMMAND\" +# or rshell LINECARD +# """ +# print(cli.__doc__) \ No newline at end of file diff --git a/rcli/rexec.py b/rcli/rexec.py index 5024593182..fb56df8351 100644 --- a/rcli/rexec.py +++ b/rcli/rexec.py @@ -1,54 +1,44 @@ import os import click import paramiko +import sys from .linecard import Linecard -from .utils import get_all_linecards, get_password, get_password_from_file +from rcli import utils as rcli_utils +from sonic_py_common import device_info @click.command() -@click.argument('linecard_names', nargs=-1, type=str, required=True, autocompletion=get_all_linecards) +@click.argument('linecard_names', nargs=-1, type=str, required=True) @click.option('-c', '--command', type=str, required=True) -@click.option('-k','--use-ssh-keys/--no-keys', default=False) -@click.option('-p','--password-filename', type=str) -def cli(linecard_names, command, use_ssh_keys=False, password_filename=None): +def cli(linecard_names, command): """ Executes a command on one or many linecards :param linecard_names: A list of linecard names to execute the command on, use `all` to execute on all linecards. :param command: The command to execute on the linecard(s) - :param use_ssh_keys: If True, will attempt to use ssh keys to login to the - linecard. If False, will prompt for password, defaults to False (optional) - :param password_filename: A file containing the password for the linecard. If - not provided inline, user will be prompted for password. File should be - relative to current path. """ + if not device_info.is_chassis(): + click.echo("This commmand is only supported Chassis") + sys.exit(1) + username = os.getlogin() + password = rcli_utils.get_password(username) if list(linecard_names) == ["all"]: # Get all linecard names using autocompletion helper - linecard_names = get_all_linecards(None, None, "") - - if use_ssh_keys: - # If we want to use ssh keys, check if the user provided a password - password = None if not password_filename else get_password_from_file(password_filename) - elif password_filename: - # Don't use ssh keys and read password from file - password = get_password_from_file(password_filename) - else: - # Password filename was not provided, read password from user input - password = get_password(username) + linecard_names = rcli_utils.get_all_linecards(None, None, "") # Iterate through each linecard, execute command, and gather output for linecard_name in linecard_names: try: - lc = Linecard(linecard_name, username, password, use_ssh_keys) + lc = Linecard(linecard_name, username, password) if lc.connection: # If connection was created, connection exists. Otherwise, user will see an error message. click.echo("======== {} output: ========".format(lc.linecard_name)) click.echo(lc.execute_cmd(command)) except paramiko.ssh_exception.AuthenticationException: - click.echo("Login failed on '{}' with username '{}'".format(linecard_name, username)) + click.echo("Login failed on '{}' with username '{}'".format(linecard_name, lc.username)) if __name__=="__main__": cli(prog_name='rexec') diff --git a/rcli/rshell.py b/rcli/rshell.py index 0fd137c1d1..decda6cd59 100644 --- a/rcli/rshell.py +++ b/rcli/rshell.py @@ -1,43 +1,37 @@ import os import click import paramiko +import sys from .linecard import Linecard -from .utils import get_all_linecards, get_password, get_password_from_file +from sonic_py_common import device_info +from rcli import utils as rcli_utils + @click.command() -@click.argument('linecard_name', type=str, autocompletion=get_all_linecards) -@click.option('-k','--use-ssh-keys/--no-keys', default=False) -@click.option('-p','--password-filename', type=str) -def cli(linecard_name, use_ssh_keys=False,password_filename=None): +@click.argument('linecard_name', type=str, autocompletion=rcli_utils.get_all_linecards) +def cli(linecard_name): """ Open interactive shell for one linecard :param linecard_name: The name of the linecard to connect to - :param use_ssh_keys: If True, will attempt to use ssh keys to login to the - linecard. If False, will prompt for password, defaults to False (optional) - :param password_filename: The password for the linecard, if not provided inline, - user will be prompted for password """ - username = os.getlogin() + if not device_info.is_chassis(): + click.echo("This commmand is only supported Chassis") + sys.exit(1) - if use_ssh_keys: - # If we want to use ssh keys, check if the user provided a password - password = None if not password_filename else get_password_from_file(password_filename) - elif password_filename: - # Don't use ssh keys and read password from file - password = get_password_from_file(password_filename) - else: - # Password filename was not provided, read password from user input - password = get_password(username) - + username = os.getlogin() + password = rcli_utils.get_password(username) + try: - lc = Linecard(linecard_name, username, password, use_ssh_keys) + lc =Linecard(linecard_name, username, password) if lc.connection: + click.echo("Connecting to {}".format(lc.linecard_name)) # If connection was created, connection exists. Otherwise, user will see an error message. lc.start_shell() + click.echo("Connection Closed") except paramiko.ssh_exception.AuthenticationException: - click.echo("Login failed on '{}' with username '{}'".format(linecard_name, username)) + click.echo("Login failed on '{}' with username '{}'".format(linecard_name, lc.username)) if __name__=="__main__": diff --git a/rcli/utils.py b/rcli/utils.py index 0518c95d1a..933043d069 100644 --- a/rcli/utils.py +++ b/rcli/utils.py @@ -1,6 +1,7 @@ import click from getpass import getpass import os +import sys from swsscommon.swsscommon import SonicV2Connector @@ -15,6 +16,35 @@ CHASSIS_MIDPLANE_INFO_IP_FIELD = 'ip_address' CHASSIS_MIDPLANE_INFO_ACCESS_FIELD = 'access' +CHASSIS_MODULE_HOSTNAME_TABLE = 'CHASSIS_MODULE_HOSTNAME_TABLE' +CHASSIS_MODULE_HOSTNAME = 'module_hostname' + +def connect_to_chassis_state_db(): + chassis_state_db = SonicV2Connector(host="127.0.0.1") + chassis_state_db.connect(chassis_state_db.CHASSIS_STATE_DB) + return chassis_state_db + + +def connect_state_db(): + state_db = SonicV2Connector(host="127.0.0.1") + state_db.connect(state_db.STATE_DB) + return state_db + + + +def get_linecard_module_name_from_hostname(linecard_name: str): + + chassis_state_db = connect_to_chassis_state_db() + + keys = chassis_state_db.keys(chassis_state_db.CHASSIS_STATE_DB , '{}|{}'.format(CHASSIS_MODULE_HOSTNAME_TABLE, '*')) + for key in keys: + module_name = key.split('|')[1] + hostname = chassis_state_db.get(chassis_state_db.CHASSIS_STATE_DB, key, CHASSIS_MODULE_HOSTNAME) + if hostname.replace('-', '').lower() == linecard_name.replace('-', '').lower(): + return module_name + + return None + def get_linecard_ip(linecard_name: str): """ Given a linecard name, lookup its IP address in the midplane table @@ -26,30 +56,36 @@ def get_linecard_ip(linecard_name: str): # Adapted from `show chassis modules midplane-status` command logic: # https://github.com/sonic-net/sonic-utilities/blob/master/show/chassis_modules.py - state_db = SonicV2Connector(host="127.0.0.1") - state_db.connect(state_db.STATE_DB) - - key_pattern = '*' - - keys = state_db.keys(state_db.STATE_DB, CHASSIS_MIDPLANE_INFO_TABLE + key_pattern) - if not keys: - click.echo('{} table is empty'.format(CHASSIS_MIDPLANE_INFO_TABLE)) + # if the user passes linecard hostname, then try to get the module name for that linecard + module_name = get_linecard_module_name_from_hostname(linecard_name) + # if the module name cannot be found from host, assume the user has passed module name + if module_name is None: + module_name = linecard_name + module_ip, module_access = get_module_ip_and_access_from_state_db(module_name) + + if not module_ip: + click.echo('Linecard {} not found'.format(linecard_name)) return None - for key in keys: - key_list = key.split('|') - if len(key_list) != 2: # error data in DB, log it and ignore - click.echo('Warn: Invalid Key {} in {} table'.format(key, CHASSIS_MIDPLANE_INFO_TABLE)) - continue - - data_dict = state_db.get_all(state_db.STATE_DB, key) - ip = data_dict[CHASSIS_MIDPLANE_INFO_IP_FIELD] - - if key_list[1].lower().replace("-","") == linecard_name.lower().replace("-",""): - return ip + if module_access != 'True': + click.echo('Linecard {} not accessible'.format(linecard_name)) + return None + - # Not able to find linecard in table - return None + return module_ip + +def get_module_ip_and_access_from_state_db(module_name): + state_db = connect_state_db() + data_dict = state_db.get_all( + state_db.STATE_DB, '{}|{}'.format(CHASSIS_MIDPLANE_INFO_TABLE,module_name )) + if data_dict is None: + return None, None + + linecard_ip = data_dict.get(CHASSIS_MIDPLANE_INFO_IP_FIELD, None) + access = data_dict.get(CHASSIS_MIDPLANE_INFO_ACCESS_FIELD, None) + + return linecard_ip, access + def get_all_linecards(ctx, args, incomplete) -> list: """ @@ -64,48 +100,37 @@ def get_all_linecards(ctx, args, incomplete) -> list: # Adapted from `show chassis modules midplane-status` command logic: # https://github.com/sonic-net/sonic-utilities/blob/master/show/chassis_modules.py - state_db = SonicV2Connector(host="127.0.0.1") - state_db.connect(state_db.STATE_DB) - - key_pattern = '*' - - keys = state_db.keys(state_db.STATE_DB, CHASSIS_MIDPLANE_INFO_TABLE + key_pattern) - if not keys: - click.echo('{} table is empty'.format(CHASSIS_MIDPLANE_INFO_TABLE)) - return [] - + + chassis_state_db = connect_to_chassis_state_db() + state_db = connect_state_db() + linecards = [] - + keys = state_db.keys(state_db.STATE_DB,'{}|*'.format(CHASSIS_MIDPLANE_INFO_TABLE)) for key in keys: key_list = key.split('|') if len(key_list) != 2: # error data in DB, log it and ignore - click.echo('Warn: Invalid Key {} in {} table'.format(key, CHASSIS_MIDPLANE_INFO_TABLE)) + click.echo('Warn: Invalid Key {} in {} table'.format(key, CHASSIS_MIDPLANE_INFO_TABLE )) continue - - data_dict = state_db.get_all(state_db.STATE_DB, key) - linecard_name = key_list[1].lower().replace("-","") - linecard_ip = data_dict[CHASSIS_MIDPLANE_INFO_IP_FIELD] - access = data_dict[CHASSIS_MIDPLANE_INFO_ACCESS_FIELD] - - if access == "True": - linecards.append(linecard_name) + module_name = key_list[1] + linecard_ip, access = get_module_ip_and_access_from_state_db(module_name) + if linecard_ip is None: + continue + + if access != "True" : + continue + + # get the hostname for this module + hostname = chassis_state_db.get(chassis_state_db.CHASSIS_STATE_DB, '{}|{}'.format(CHASSIS_MODULE_HOSTNAME_TABLE, module_name), CHASSIS_MODULE_HOSTNAME) + if hostname: + linecards.append(hostname) + else: + linecards.append(module_name) # Return a list of all matched linecards return [lc for lc in linecards if incomplete in lc] -def get_password_from_file(password_filename: str) -> str: - """ - Read the password from the file and return - - :param password_filename: The path to the file containing the password, if - not provided the user will be prompted for password - :type password_filename: str - :return: The password for the username - """ - with open(os.path.expanduser(password_filename), "r") as file: - return file.read().replace("\n","") -def get_password(username: str) -> str: +def get_password(username=None): """ Prompts the user for a password, and returns the password @@ -113,7 +138,10 @@ def get_password(username: str) -> str: :type username: str :return: The password for the username. """ - return "123456" + + if username is None: + username =os.getlogin() + return getpass( "Password for username '{}': ".format(username), # Pass in click stdout stream - this is similar to using click.echo diff --git a/tests/chassis_modules_test.py b/tests/chassis_modules_test.py index e6dbe569d2..fa8cd608dd 100644 --- a/tests/chassis_modules_test.py +++ b/tests/chassis_modules_test.py @@ -33,11 +33,11 @@ """ show_chassis_midplane_output="""\ - Name IP-Address Reachability ------------ ------------- -------------- - LINE-CARD0 192.168.1.1 True - LINE-CARD1 192.168.1.2 False -SUPERVISOR0 192.168.1.100 True + Name IP-Address Reachability +---------- ------------- -------------- +LINE-CARD0 192.168.1.100 True +LINE-CARD1 192.168.1.2 False +LINE-CARD2 192.168.1.1 True """ show_chassis_system_ports_output_asic0="""\ @@ -225,7 +225,7 @@ def test_midplane_show_all_count_lines(self): result = runner.invoke(show.cli.commands["chassis"].commands["modules"].commands["midplane-status"], []) print(result.output) result_lines = result.output.strip('\n').split('\n') - modules = ["LINE-CARD0", "LINE-CARD1", "SUPERVISOR0"] + modules = ["LINE-CARD0", "LINE-CARD1", "LINE-CARD2"] for i, module in enumerate(modules): assert module in result_lines[i + warning_lines + header_lines] assert len(result_lines) == warning_lines + header_lines + len(modules) diff --git a/tests/mock_tables/chassis_state_db.json b/tests/mock_tables/chassis_state_db.json new file mode 100644 index 0000000000..5178c49ca0 --- /dev/null +++ b/tests/mock_tables/chassis_state_db.json @@ -0,0 +1,9 @@ +{ + "CHASSIS_MODULE_HOSTNAME_TABLE|LINE-CARD0": { + "module_hostname": "sonic-lc1" + }, + "CHASSIS_MODULE_HOSTNAME_TABLE|LINE-CARD1": { + "module_hostname": "sonic-lc2" + } + +} \ No newline at end of file diff --git a/tests/mock_tables/database_config.json b/tests/mock_tables/database_config.json index d12ba05414..f55c0734c2 100644 --- a/tests/mock_tables/database_config.json +++ b/tests/mock_tables/database_config.json @@ -56,6 +56,11 @@ "id" : 12, "separator": "|", "instance" : "redis" + }, + "CHASSIS_STATE_DB" : { + "id" : 13, + "separator": "|", + "instance" : "redis" } }, "VERSION" : "1.1" diff --git a/tests/remote_cli_test.py b/tests/remote_cli_test.py index d545540740..fde8a5a09b 100644 --- a/tests/remote_cli_test.py +++ b/tests/remote_cli_test.py @@ -1,93 +1,254 @@ -# import os -# from unittest import mock -# from click.testing import CliRunner -# from swsscommon.swsscommon import SonicV2Connector -# from .mock_tables import dbconnector -# from utilities_common.db import Db - -# import show.main as show -# import rcli -# from rcli import rexec -# from rcli import rshell - -# test_path = os.path.dirname(os.path.abspath(__file__)) -# mock_db_path = os.path.join(test_path, "remote_cli") -# header_lines = 2 - - -# class TestRemoteCLI(object): -# @classmethod -# def setup_class(cls): -# print("SETUP") -# os.environ["UTILITIES_UNIT_TESTING"] = "1" - -# def set_db_values(self, db, key, kvs): -# for field, value in kvs.items(): -# db.set(db.STATE_DB, key, field, value) - -# # @mock.patch("rcli.utils.get_linecard_ip", mock.MagicMock(return_value="1.1.1.1")) -# @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) -# @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="123456")) -# def test_rexec_echo(self): -# runner = CliRunner() -# db = Db() -# dbconnector = db.db -# LINECARD_NAME = "LINE-CARD0" - -# runner = CliRunner() -# result = runner.invoke(show.cli.commands["chassis"].commands["modules"].commands["midplane-status"], obj=db) -# result_lines = result.output.strip('\n').split('\n') -# assert result.exit_code == 0 -# result_out = (result_lines[header_lines]).split() -# print(result_out) -# assert result_out[2] == 'True', "Unable to find LINE-CARD0 in CHASSIS_MIDPLANE_TABLE" - -# linecard_instance = mock.MagicMock() -# linecard_instance.connection = True -# linecard_instance.linecard_name = LINECARD_NAME -# linecard_instance.execute_cmd.return_value = "hello world\n" - - -# rcli.utils.get_linecard_ip = mock.MagicMock(return_value="1.1.1.1") +import os +from click.testing import CliRunner +import paramiko +from rcli import rexec +from rcli import rshell +from rcli import linecard +from rcli import utils as rcli_utils +import sys +from io import BytesIO, StringIO +from unittest import mock +import select +import socket + +MULTI_LC_REXEC_OUTPUT = '''======== sonic-lc1 output: ======== +hello world +======== LINE-CARD2 output: ======== +hello world +''' +REXEC_HELP = '''Usage: cli [OPTIONS] LINECARD_NAMES... + + Executes a command on one or many linecards + + :param linecard_names: A list of linecard names to execute the command on, + use `all` to execute on all linecards. :param command: The command to + execute on the linecard(s) + +Options: + -c, --command TEXT [required] + --help Show this message and exit. +''' + +def mock_exec_command(): + + mock_stdout = BytesIO(b"""hello world""") + mock_stderr = BytesIO() + return '', mock_stdout, None + +def mock_exec_error_cmd(): + mock_stdout = BytesIO() + mock_stderr = BytesIO(b"""Command not found""") + return '', mock_stdout, mock_stderr + +def mock_connection_channel(): + c = mock.MagicMock(return_value="channel") + c.get_pty = mock.MagicMock(return_value='') + c.invoke_shell = mock.MagicMock() + c.recv = mock.MagicMock(side_effect=['abcd', '']) + return c + +def mock_connection_channel_with_timeout(): + c = mock.MagicMock(return_value="channel") + c.get_pty = mock.MagicMock(return_value='') + c.invoke_shell = mock.MagicMock() + c.recv = mock.MagicMock(side_effect=['abcd', socket.timeout(10, 'timeout')]) + return c + +def mock_paramiko_connection(channel): + # Create a mock to return for connection. + conn = mock.MagicMock() + #create a mock return for transport + t = mock.MagicMock() + t.open_session = mock.MagicMock(return_value=channel) + conn.get_transport = mock.MagicMock(return_value=t) + conn.connect = mock.MagicMock() + conn.close = mock.MagicMock() + return conn + +class TestRemoteExec(object): + @classmethod + def setup_class(cls): + print("SETUP") + from .mock_tables import dbconnector + dbconnector.load_database_config() + + @mock.patch("sonic_py_common.device_info.is_chassis", mock.MagicMock(return_value=True)) + @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) + @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="dummy")) + #@mock.patch.object(linecard.Linecard, '_get_password', mock.MagicMock(return_value='dummmy')) + @mock.patch.object(paramiko.SSHClient, 'connect', mock.MagicMock()) + @mock.patch.object(paramiko.SSHClient, 'exec_command', mock.MagicMock(return_value = mock_exec_command())) + def test_rexec_with_module_name(self): + runner = CliRunner() + LINECARD_NAME = "LINE-CARD0" + result = runner.invoke(rexec.cli, [LINECARD_NAME, "-c", "pwd"]) + print(result.output) + assert result.exit_code == 0, result.output + assert "hello world" in result.output + + @mock.patch("sonic_py_common.device_info.is_chassis", mock.MagicMock(return_value=True)) + @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) + @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="dummy")) + @mock.patch.object(paramiko.SSHClient, 'connect', mock.MagicMock()) + @mock.patch.object(paramiko.SSHClient, 'exec_command', mock.MagicMock(return_value = mock_exec_command())) + def test_rexec_with_hostname(self): + runner = CliRunner() + LINECARD_NAME = "sonic-lc1" + result = runner.invoke(rexec.cli, [LINECARD_NAME, "-c", "pwd"]) + print(result.output) + assert result.exit_code == 0, result.output + assert "hello world" in result.output + + @mock.patch("sonic_py_common.device_info.is_chassis", mock.MagicMock(return_value=True)) + @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) + @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="dummy")) + @mock.patch.object(paramiko.SSHClient, 'connect', mock.MagicMock()) + @mock.patch.object(paramiko.SSHClient, 'exec_command', mock.MagicMock(return_value = mock_exec_error_cmd())) + def test_rexec_error_with_module_name(self): + runner = CliRunner() + LINECARD_NAME = "LINE-CARD0" + result = runner.invoke(rexec.cli, [LINECARD_NAME, "-c", "pwd"]) + print(result.output) + assert result.exit_code == 0, result.output + assert "Command not found" in result.output - -# print(dir(rcli.utils)) - -# result = runner.invoke(rexec.cli, [LINECARD_NAME, "-c", "pwd"], obj=db) -# print(result.output) -# assert result.exit_code == 0, result.output -# assert "hello world" in result.output - -# # with runner.isolated_filesystem(): -# # with open('password.txt', 'w') as f: -# # f.write('123456') -# # result = runner.invoke(rexec.cli, [LINECARD_NAME, "-c", "pwd"]) -# # print(result) -# # assert result.exit_code == 0, result.output -# # assert "hello world" in result.output - -# # @mock.patch("paramiko") -# # def test_rexec_echo(self, paramiko): -# # runner = CliRunner() -# # db = Db() -# # dbconnector = db.db -# # LINECARD_NAME = "LINE-CARD0" - -# # runner = CliRunner() -# # result = runner.invoke(show.cli.commands["chassis"].commands["modules"].commands["midplane-status"], [LINECARD_NAME], obj=db) -# # result_lines = result.output.strip('\n').split('\n') -# # assert result.exit_code == 0 -# # result_out = (result_lines[header_lines]).split() -# # assert result_out[2] == 'True', "Unable to find LINE-CARD0 in CHASSIS_MIDPLANE_TABLE" - -# # ssh_client_instance = mock.MagicMock() -# # ssh_client_instance.connection = mock.MagicMock() - -# # must mock for all test cases -# # ssh_client_instance.connection.exec_command.return_value - -# # ssh-copy-id -# # mock_os = mock.MagicMock() -# # mock_os.exists.return_value = True - - \ No newline at end of file + def test_rexec_error(self): + runner = CliRunner() + LINECARD_NAME = "LINE-CARD0" + result = runner.invoke(rexec.cli, [LINECARD_NAME, "-c", "show version"]) + print(result.output) + assert result.exit_code == 1, result.output + assert "This commmand is only supported Chassis" in result.output + + @mock.patch("sonic_py_common.device_info.is_chassis", mock.MagicMock(return_value=True)) + @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) + @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="dummy")) + @mock.patch.object(paramiko.SSHClient, 'connect', mock.MagicMock()) + @mock.patch.object(linecard.Linecard, 'execute_cmd', mock.MagicMock(return_value = "hello world")) + def test_rexec_all(self): + runner = CliRunner() + LINECARD_NAME = "all" + result = runner.invoke(rexec.cli, [LINECARD_NAME, "-c", "show version"]) + print(result.output) + assert result.exit_code == 0, result.output + assert MULTI_LC_REXEC_OUTPUT == result.output + + @mock.patch("sonic_py_common.device_info.is_chassis", mock.MagicMock(return_value=True)) + @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) + @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="dummy")) + @mock.patch.object(paramiko.SSHClient, 'connect', mock.MagicMock()) + @mock.patch.object(linecard.Linecard, 'execute_cmd', mock.MagicMock(return_value = "hello world")) + def test_rexec_invalid_lc(self): + runner = CliRunner() + LINECARD_NAME = "sonic-lc-3" + result = runner.invoke(rexec.cli, [LINECARD_NAME, "-c", "show version"]) + print(result.output) + assert result.exit_code == 1, result.output + assert "Linecard sonic-lc-3 not found\n" == result.output + + + @mock.patch("sonic_py_common.device_info.is_chassis", mock.MagicMock(return_value=True)) + @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) + @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="dummy")) + @mock.patch.object(paramiko.SSHClient, 'connect', mock.MagicMock()) + @mock.patch.object(linecard.Linecard, 'execute_cmd', mock.MagicMock(return_value = "hello world")) + def test_rexec_unreachable_lc(self): + runner = CliRunner() + LINECARD_NAME = "LINE-CARD1" + result = runner.invoke(rexec.cli, [LINECARD_NAME, "-c", "show version"]) + print(result.output) + assert result.exit_code == 1, result.output + assert "Linecard LINE-CARD1 not accessible\n" == result.output + + @mock.patch("sonic_py_common.device_info.is_chassis", mock.MagicMock(return_value=True)) + @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) + @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="dummy")) + @mock.patch.object(paramiko.SSHClient, 'connect', mock.MagicMock()) + @mock.patch.object(linecard.Linecard, 'execute_cmd', mock.MagicMock(return_value = "hello world")) + def test_rexec_help(self): + runner = CliRunner() + LINECARD_NAME = "LINE-CARD1" + result = runner.invoke(rexec.cli, ["--help"]) + print(result.output) + assert result.exit_code == 0, result.output + assert REXEC_HELP == result.output + + @mock.patch("sonic_py_common.device_info.is_chassis", mock.MagicMock(return_value=True)) + @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) + @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="dummy")) + @mock.patch.object(paramiko.SSHClient, 'connect', mock.MagicMock(side_effect=paramiko.ssh_exception.NoValidConnectionsError({('192.168.0.1', + 22): "None" }))) + @mock.patch.object(linecard.Linecard, 'execute_cmd', mock.MagicMock(return_value = "hello world")) + def test_rexec_exception(self): + runner = CliRunner() + LINECARD_NAME = "sonic-lc1" + result = runner.invoke(rexec.cli, [LINECARD_NAME, "-c", "show version"]) + print(result.output) + assert result.exit_code == 0, result.output + assert "[Errno None] Unable to connect to port 22 on 192.168.0.1\n" == result.output + + +class TestRemoteCLI(object): + @classmethod + def setup_class(cls): + print("SETUP") + from .mock_tables import dbconnector + dbconnector.load_database_config() + + @mock.patch("sonic_py_common.device_info.is_chassis", mock.MagicMock(return_value=True)) + @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) + @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="dummy")) + @mock.patch.object(linecard.Linecard, '_set_tty_params', mock.MagicMock()) + #@mock.patch.object(linecard.Linecard, '_is_data_to_read', mock.MagicMock(return_value=True)) + def test_rcli_with_module_name(self): + runner = CliRunner() + LINECARD_NAME = "LINE-CARD0" + channel = mock_connection_channel() + + with mock.patch('paramiko.SSHClient', mock.MagicMock(return_value=mock_paramiko_connection(channel))), \ + mock.patch('select.select', mock.MagicMock(return_value=([channel], [], []))): + result = runner.invoke(rshell.cli, [LINECARD_NAME]) + print(result.output) + assert result.exit_code == 0, result.output + assert "abcd" in result.output + + + @mock.patch("sonic_py_common.device_info.is_chassis", mock.MagicMock(return_value=True)) + @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) + @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="dummy")) + @mock.patch.object(linecard.Linecard, '_set_tty_params', mock.MagicMock()) + def test_rcli_with_module_name_2(self): + runner = CliRunner() + LINECARD_NAME = "LINE-CARD0" + channel = mock_connection_channel() + + with mock.patch('paramiko.SSHClient', mock.MagicMock(return_value=mock_paramiko_connection(channel))), \ + mock.patch('select.select', mock.MagicMock(side_effect=[([], [], []), ([channel], [], []),([channel], [], [])])): + result = runner.invoke(rshell.cli, [LINECARD_NAME]) + print(result.output) + assert result.exit_code == 0, result.output + assert "abcd" in result.output + + @mock.patch("sonic_py_common.device_info.is_chassis", mock.MagicMock(return_value=True)) + @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) + @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="dummy")) + @mock.patch.object(linecard.Linecard, '_set_tty_params', mock.MagicMock()) + def test_rcli_with_module_name_3(self): + runner = CliRunner() + LINECARD_NAME = "LINE-CARD0" + channel = mock_connection_channel_with_timeout() + + with mock.patch('paramiko.SSHClient', mock.MagicMock(return_value=mock_paramiko_connection(channel))), \ + mock.patch('select.select', mock.MagicMock(return_value=([channel], [], []))): + result = runner.invoke(rshell.cli, [LINECARD_NAME]) + print(result.output) + assert result.exit_code == 0, result.output + assert "abcd" in result.output + + def test_rcli_error(self): + runner = CliRunner() + LINECARD_NAME = "LINE-CARD0" + result = runner.invoke(rshell.cli, [LINECARD_NAME]) + print(result.output) + assert result.exit_code == 1, result.output + assert "This commmand is only supported Chassis" in result.output \ No newline at end of file From f108b95ca152f090d77b016757b5933398350aa7 Mon Sep 17 00:00:00 2001 From: Arvindsrinivasan Lakshmi Narasimhan Date: Tue, 18 Apr 2023 18:52:00 +0000 Subject: [PATCH 3/5] fixes from testing Signed-off-by: Arvindsrinivasan Lakshmi Narasimhan --- rcli/linecard.py | 7 +++---- setup.py | 2 +- tests/mock_tables/state_db.json | 4 ++-- tests/remote_cli_test.py | 12 +++++++++--- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/rcli/linecard.py b/rcli/linecard.py index 8a0f2d0059..fdc6882ed1 100644 --- a/rcli/linecard.py +++ b/rcli/linecard.py @@ -80,12 +80,11 @@ def _is_data_to_write(self, read): def _write_to_terminal(self, data): # Write channel output to terminal - sys.stdout.write(data.decode()) + sys.stdout.write(data) sys.stdout.flush() def _start_interactive_shell(self): - # #import pdb; pdb.set_trace() - # oldtty = termios.tcgetattr(sys.stdin) + oldtty = termios.tcgetattr(sys.stdin) try: self._set_tty_params() self.channel.settimeout(0.0) @@ -113,7 +112,7 @@ def _start_interactive_shell(self): self.channel.send(x) finally: # Now that the channel has been exited, return to the previously-saved old tty - #termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) pass diff --git a/setup.py b/setup.py index fe89026d86..daf74a79bb 100644 --- a/setup.py +++ b/setup.py @@ -209,7 +209,7 @@ 'natsort>=6.2.1', # 6.2.1 is the last version which supports Python 2. Can update once we no longer support Python 2 'netaddr>=0.8.0', 'netifaces>=0.10.7', - 'paramiko>=2.11.0', + 'paramiko==2.11.0', 'pexpect>=4.8.0', 'semantic-version>=2.8.5', 'prettyprinter>=0.18.0', diff --git a/tests/mock_tables/state_db.json b/tests/mock_tables/state_db.json index 12552997b9..0b2f64040b 100644 --- a/tests/mock_tables/state_db.json +++ b/tests/mock_tables/state_db.json @@ -734,11 +734,11 @@ "max_queues": "20", "max_priority_groups": "8" }, - "CHASSIS_MIDPLANE_TABLE|SUPERVISOR0": { + "CHASSIS_MIDPLANE_TABLE|LINE-CARD0": { "ip_address": "192.168.1.100", "access": "True" }, - "CHASSIS_MIDPLANE_TABLE|LINE-CARD0": { + "CHASSIS_MIDPLANE_TABLE|LINE-CARD2": { "ip_address": "192.168.1.1", "access": "True" }, diff --git a/tests/remote_cli_test.py b/tests/remote_cli_test.py index fde8a5a09b..67545dd1b3 100644 --- a/tests/remote_cli_test.py +++ b/tests/remote_cli_test.py @@ -10,6 +10,7 @@ from unittest import mock import select import socket +import termios MULTI_LC_REXEC_OUTPUT = '''======== sonic-lc1 output: ======== hello world @@ -199,7 +200,8 @@ def setup_class(cls): @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="dummy")) @mock.patch.object(linecard.Linecard, '_set_tty_params', mock.MagicMock()) - #@mock.patch.object(linecard.Linecard, '_is_data_to_read', mock.MagicMock(return_value=True)) + @mock.patch.object(termios, 'tcsetattr', mock.MagicMock()) + @mock.patch.object(termios, 'tcgetattr', mock.MagicMock(return_value=[])) def test_rcli_with_module_name(self): runner = CliRunner() LINECARD_NAME = "LINE-CARD0" @@ -217,6 +219,8 @@ def test_rcli_with_module_name(self): @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="dummy")) @mock.patch.object(linecard.Linecard, '_set_tty_params', mock.MagicMock()) + @mock.patch.object(termios, 'tcsetattr', mock.MagicMock()) + @mock.patch.object(termios, 'tcgetattr', mock.MagicMock(return_value=[])) def test_rcli_with_module_name_2(self): runner = CliRunner() LINECARD_NAME = "LINE-CARD0" @@ -227,12 +231,14 @@ def test_rcli_with_module_name_2(self): result = runner.invoke(rshell.cli, [LINECARD_NAME]) print(result.output) assert result.exit_code == 0, result.output - assert "abcd" in result.output + assert "Connecting to LINE-CARD0" in result.output @mock.patch("sonic_py_common.device_info.is_chassis", mock.MagicMock(return_value=True)) @mock.patch("os.getlogin", mock.MagicMock(return_value="admin")) @mock.patch("rcli.utils.get_password", mock.MagicMock(return_value="dummy")) @mock.patch.object(linecard.Linecard, '_set_tty_params', mock.MagicMock()) + @mock.patch.object(termios, 'tcsetattr', mock.MagicMock()) + @mock.patch.object(termios, 'tcgetattr', mock.MagicMock(return_value=[])) def test_rcli_with_module_name_3(self): runner = CliRunner() LINECARD_NAME = "LINE-CARD0" @@ -243,7 +249,7 @@ def test_rcli_with_module_name_3(self): result = runner.invoke(rshell.cli, [LINECARD_NAME]) print(result.output) assert result.exit_code == 0, result.output - assert "abcd" in result.output + assert "Connecting to LINE-CARD0" in result.output def test_rcli_error(self): runner = CliRunner() From f5f204affba2c8348a7c5118fd6de6eda93f2729 Mon Sep 17 00:00:00 2001 From: Arvindsrinivasan Lakshmi Narasimhan Date: Tue, 18 Apr 2023 21:38:57 +0000 Subject: [PATCH 4/5] remove extra files Signed-off-by: Arvindsrinivasan Lakshmi Narasimhan --- rcli/get_all_bgp_status.py | 8 ---- rcli/interactive.py | 76 -------------------------------------- rcli/main.py | 18 --------- 3 files changed, 102 deletions(-) delete mode 100644 rcli/get_all_bgp_status.py delete mode 100644 rcli/interactive.py delete mode 100644 rcli/main.py diff --git a/rcli/get_all_bgp_status.py b/rcli/get_all_bgp_status.py deleted file mode 100644 index 8a72b5044c..0000000000 --- a/rcli/get_all_bgp_status.py +++ /dev/null @@ -1,8 +0,0 @@ -from click.testing import CliRunner -from rcli import rexec - -runner = CliRunner() - -result = runner.invoke(rexec.cli, ["all", "-c", "show ip bgp summary", "-p","password.txt"]) - -print(result.output.strip("\n")) \ No newline at end of file diff --git a/rcli/interactive.py b/rcli/interactive.py deleted file mode 100644 index 85a91aec94..0000000000 --- a/rcli/interactive.py +++ /dev/null @@ -1,76 +0,0 @@ -# From https://github.com/paramiko/paramiko/blob/main/demos/interactive.py -# -####################################################################### -# -# Copyright (C) 2003-2007 Robey Pointer -# -# This file is part of paramiko. -# -# Paramiko is free software; you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -import select -import socket -import sys -import termios -import tty - -from paramiko.py3compat import u -from paramiko import Channel - - -def interactive_shell(channel: Channel): - """ - Continuously wait for commands and execute them - - The function is a loop that waits for input from either the channel or the terminal. If input is - received from the channel, it is printed to the terminal. If input is received from the terminal, it - is sent to the channel. - - :param channel: The channel object we use to communicate with the linecard - :type channel: paramiko.Channel - """ - # Save the current tty so we can return to it later - oldtty = termios.tcgetattr(sys.stdin) - try: - tty.setraw(sys.stdin.fileno()) - tty.setcbreak(sys.stdin.fileno()) - channel.settimeout(0.0) - - while True: - # Continuously wait for commands and execute them - r, w, e = select.select([channel, sys.stdin], [], []) - if channel in r: - try: - # Get output from channel - x = u(channel.recv(1024)) - if len(x) == 0: - # logout message will be displayed - break - # Write channel output to terminal - sys.stdout.write(x) - sys.stdout.flush() - except socket.timeout: - pass - if sys.stdin in r: - # If we are able to send input, get the input from stdin - x = sys.stdin.read(1) - if len(x) == 0: - break - # Send the input to the channel - channel.send(x) - - finally: - # Now that the channel has been exited, return to the previously-saved old tty - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, oldtty) \ No newline at end of file diff --git a/rcli/main.py b/rcli/main.py deleted file mode 100644 index b8c04e7036..0000000000 --- a/rcli/main.py +++ /dev/null @@ -1,18 +0,0 @@ -# import click - - -# # -# # 'rcli' group (root group) -# # - -# # This is our entrypoint - the main "show" command -# @click.command() -# # @click.pass_context -# def cli(): -# """ -# SONiC command line - 'rcli' command. - -# Usage: rexec LINECARDS -c \"COMMAND\" -# or rshell LINECARD -# """ -# print(cli.__doc__) \ No newline at end of file From 51de69c37dea2e7cb553abe2488c8b3ece592597 Mon Sep 17 00:00:00 2001 From: Arvindsrinivasan Lakshmi Narasimhan Date: Tue, 18 Apr 2023 22:24:58 +0000 Subject: [PATCH 5/5] remove extra file Signed-off-by: Arvindsrinivasan Lakshmi Narasimhan --- show/update_etchosts.py | 63 ----------------------------------------- 1 file changed, 63 deletions(-) delete mode 100644 show/update_etchosts.py diff --git a/show/update_etchosts.py b/show/update_etchosts.py deleted file mode 100644 index 8f13fe8df1..0000000000 --- a/show/update_etchosts.py +++ /dev/null @@ -1,63 +0,0 @@ -import click - -import utilities_common.cli as clicommon -from swsscommon.swsscommon import SonicV2Connector - -from natsort import natsorted -from tabulate import tabulate - -CHASSIS_MODULE_INFO_TABLE = 'CHASSIS_MODULE_TABLE' -CHASSIS_MODULE_INFO_KEY_TEMPLATE = 'CHASSIS_MODULE {}' -CHASSIS_MODULE_INFO_DESC_FIELD = 'desc' -CHASSIS_MODULE_INFO_SLOT_FIELD = 'slot' -CHASSIS_MODULE_INFO_OPERSTATUS_FIELD = 'oper_status' -CHASSIS_MODULE_INFO_ADMINSTATUS_FIELD = 'admin_status' - -CHASSIS_MIDPLANE_INFO_TABLE = 'CHASSIS_MIDPLANE_TABLE' -CHASSIS_MIDPLANE_INFO_IP_FIELD = 'ip_address' -CHASSIS_MIDPLANE_INFO_ACCESS_FIELD = 'access' - -@click.command() -@clicommon.pass_db -@click.argument('chassis_module_name', metavar='', required=False) -def update(db, chassis_module_name): - """Show chassis-modules status""" - - header = ['Name', 'Description', 'Physical-Slot', 'Oper-Status', 'Admin-Status'] - chassis_cfg_table = db.cfgdb.get_table('CHASSIS_MODULE') - - state_db = SonicV2Connector(host="127.0.0.1") - state_db.connect(state_db.STATE_DB) - - key_pattern = '*' - if chassis_module_name: - key_pattern = '|' + chassis_module_name - - keys = state_db.keys(state_db.STATE_DB, CHASSIS_MODULE_INFO_TABLE + key_pattern) - if not keys: - print('Key {} not found in {} table'.format(key_pattern, CHASSIS_MODULE_INFO_TABLE)) - return - - table = [] - for key in natsorted(keys): - key_list = key.split('|') - if len(key_list) != 2: # error data in DB, log it and ignore - print('Warn: Invalid Key {} in {} table'.format(key, CHASSIS_MODULE_INFO_TABLE)) - continue - - data_dict = state_db.get_all(state_db.STATE_DB, key) - desc = data_dict[CHASSIS_MODULE_INFO_DESC_FIELD] - slot = data_dict[CHASSIS_MODULE_INFO_SLOT_FIELD] - oper_status = data_dict[CHASSIS_MODULE_INFO_OPERSTATUS_FIELD] - - admin_status = 'up' - config_data = chassis_cfg_table.get(key_list[1]) - if config_data is not None: - admin_status = config_data.get(CHASSIS_MODULE_INFO_ADMINSTATUS_FIELD) - - table.append((key_list[1], desc, slot, oper_status, admin_status)) - - if table: - click.echo(tabulate(table, header, tablefmt='simple', stralign='right')) - else: - click.echo('No data available in CHASSIS_MODULE_TABLE\n') \ No newline at end of file