diff --git a/rcli/utils.py b/rcli/utils.py index e2f48788b..7563eafdc 100644 --- a/rcli/utils.py +++ b/rcli/utils.py @@ -1,7 +1,7 @@ import click -from getpass import getpass +import getpass import os -import sys +import signal from swsscommon.swsscommon import SonicV2Connector @@ -19,6 +19,8 @@ CHASSIS_MODULE_HOSTNAME_TABLE = 'CHASSIS_MODULE_HOSTNAME_TABLE' CHASSIS_MODULE_HOSTNAME = 'module_hostname' +GET_PASSWORD_TIMEOUT = 10 + 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) @@ -151,8 +153,17 @@ def get_password(username=None): if username is None: username = os.getlogin() - return getpass( + def get_password_timeout(*args): + print("\nAborted! Timeout when waiting for password input.") + exit(1) + + signal.signal(signal.SIGALRM, get_password_timeout) + signal.alarm(GET_PASSWORD_TIMEOUT) # Set a timeout of 60 seconds + password = getpass.getpass( "Password for username '{}': ".format(username), # Pass in click stdout stream - this is similar to using click.echo stream=click.get_text_stream('stdout') ) + signal.alarm(0) # Cancel the alarm + + return password diff --git a/tests/remote_cli_test.py b/tests/remote_cli_test.py index 9883dfa16..57a220be1 100644 --- a/tests/remote_cli_test.py +++ b/tests/remote_cli_test.py @@ -11,6 +11,7 @@ import select import socket import termios +import getpass MULTI_LC_REXEC_OUTPUT = '''======== LINE-CARD0|sonic-lc1 output: ======== hello world @@ -75,17 +76,27 @@ def mock_paramiko_connection(channel): return conn +def mock_getpass(prompt="Password:", stream=None): + return "dummy" + + class TestRemoteExec(object): + __getpass = getpass.getpass + @classmethod def setup_class(cls): print("SETUP") from .mock_tables import dbconnector dbconnector.load_database_config() + getpass.getpass = mock_getpass + + @classmethod + def teardown_class(cls): + print("TEARDOWN") + getpass.getpass = TestRemoteExec.__getpass @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): @@ -98,7 +109,6 @@ def test_rexec_with_module_name(self): @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): @@ -111,7 +121,6 @@ def test_rexec_with_hostname(self): @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): @@ -133,7 +142,6 @@ def test_rexec_error(self): @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): @@ -147,7 +155,6 @@ def test_rexec_all(self): @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): @@ -161,7 +168,6 @@ def test_rexec_invalid_lc(self): @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): @@ -175,7 +181,6 @@ def test_rexec_unreachable_lc(self): @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): @@ -188,7 +193,6 @@ def test_rexec_help(self): @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")) @@ -202,7 +206,6 @@ def test_rexec_exception(self): assert "Failed to connect to sonic-lc1 with username admin\n" == result.output @mock.patch("sonic_py_common.device_info.is_chassis", mock.MagicMock(return_value=True)) - @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"}))) def test_rexec_with_user_param(self): @@ -214,6 +217,19 @@ def test_rexec_with_user_param(self): assert result.exit_code == 1, result.output assert "Failed to connect to sonic-lc1 with username testuser\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")) + def test_rexec_without_password_input(self): + runner = CliRunner() + getpass.getpass = TestRemoteExec.__getpass + LINECARD_NAME = "all" + result = runner.invoke( + rexec.cli, [LINECARD_NAME, "-c", "show version"]) + getpass.getpass = mock_getpass + print(result.output) + assert result.exit_code == 1, result.output + assert "Aborted" in result.output + class TestRemoteCLI(object): @classmethod