Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add timeout for rexec's get_password (#3484) #92

Merged
merged 1 commit into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions rcli/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import click
from getpass import getpass
import getpass
import os
import sys
import signal

from swsscommon.swsscommon import SonicV2Connector

Expand All @@ -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)
Expand Down Expand Up @@ -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
36 changes: 26 additions & 10 deletions tests/remote_cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import select
import socket
import termios
import getpass

MULTI_LC_REXEC_OUTPUT = '''======== LINE-CARD0|sonic-lc1 output: ========
hello world
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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"))
Expand All @@ -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):
Expand All @@ -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
Expand Down
Loading