From ca1de6a2b303bb6a1730878d661534c111eb4090 Mon Sep 17 00:00:00 2001 From: mkwasile Date: Wed, 10 Jun 2026 10:02:32 +0200 Subject: [PATCH] fix: handle interactive confirmation prompt in disable_pfc Dell OS10 responds to 'default interface ' with an interactive prompt: "Proceed to cleanup the interface config? [confirm yes/no]:" Netmiko's send_config_set timed out waiting for the switch prompt, raising ReadTimeout and failing test setup. Extended send_configuration() in SSHSwitchConnection with a cmd_verify parameter (default True, preserving existing behaviour). When False, commands are written to the channel without per-command echo checking, allowing a 'yes' confirmation to be pre-buffered before the switch raises the interactive prompt. Updated disable_pfc() in DellOS10 to pass cmd_verify=False with 'yes' appended to the command list for the 'default interface ' step. Updated test_disable_pfc_ndk_type and test_disable_pfc_userspace_type to assert the new call signature. Signed-off-by: Marek Wasilewski --- mfd_switchmanagement/connections/ssh.py | 13 +++++++-- .../vendors/dell/dell_os10/base.py | 11 +++++-- .../test_connections/test_ssh.py | 29 +++++++++++++++++++ .../test_dell/test_dell_os10/test_base.py | 8 +++-- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/mfd_switchmanagement/connections/ssh.py b/mfd_switchmanagement/connections/ssh.py index 29ef557..9e70aa3 100644 --- a/mfd_switchmanagement/connections/ssh.py +++ b/mfd_switchmanagement/connections/ssh.py @@ -164,17 +164,24 @@ def send_command_list(self, commands: str) -> str: logger.log(level=log_levels.OUT, msg=output) return output - def send_configuration(self, commands: List[str]) -> str: + def send_configuration(self, commands: List[str], cmd_verify: bool = True) -> str: """ Send commands list via connection as configuration. - Enter configuration mode, send commands, exit configuration mode + Enter configuration mode, send commands, exit configuration mode. :param commands: commands for send + :param cmd_verify: when False, commands are written to the channel without + waiting for echo verification between each one. Use cmd_verify=False + together with an explicit confirmation response (e.g. 'yes') appended + to *commands* when a command triggers an interactive prompt such as + Dell OS10's "Proceed to cleanup the interface config? [confirm yes/no]:" :return: Output from commands """ logger.log(level=log_levels.CMD, msg=f"Executing configuration: '{commands}'") - output = self._remote.send_config_set(commands, exit_config_mode=True, enter_config_mode=True) + output = self._remote.send_config_set( + commands, exit_config_mode=True, enter_config_mode=True, cmd_verify=cmd_verify + ) logger.log(level=log_levels.OUT, msg=output) return output diff --git a/mfd_switchmanagement/vendors/dell/dell_os10/base.py b/mfd_switchmanagement/vendors/dell/dell_os10/base.py index 5931bc1..406a19c 100644 --- a/mfd_switchmanagement/vendors/dell/dell_os10/base.py +++ b/mfd_switchmanagement/vendors/dell/dell_os10/base.py @@ -1085,8 +1085,15 @@ def disable_pfc(self, port: str, suffix: str = "100", pfc_type: str = "ndk") -> else: self.delete_qos_policy(suffix=suffix) - # Reset interface to defaults + # Reset interface to defaults. + # Dell OS10 responds to "default interface " with an interactive prompt: + # "Proceed to cleanup the interface config? [confirm yes/no]:" + # cmd_verify=False disables per-command echo verification, so the command is + # sent without waiting for its echo to be matched before proceeding. + # "yes" is appended as the interactive confirmation response; it is sent as + # a separate command and consumed by the device when the prompt appears. commands = [ f"default interface {port}", + "yes", # confirm "Proceed to cleanup the interface config? [confirm yes/no]:" ] - self._connection.send_configuration(commands) + self._connection.send_configuration(commands, cmd_verify=False) diff --git a/tests/unit/test_mfd_switchmanagement/test_connections/test_ssh.py b/tests/unit/test_mfd_switchmanagement/test_connections/test_ssh.py index f1cf8d6..3d328a8 100644 --- a/tests/unit/test_mfd_switchmanagement/test_connections/test_ssh.py +++ b/tests/unit/test_mfd_switchmanagement/test_connections/test_ssh.py @@ -194,3 +194,32 @@ def test_exit_port_configuration(self, ssh_connection, mocker): ssh_connection._connection.exit_config_mode = mocker.Mock() ssh_connection.exit_port_configuration() ssh_connection._connection.exit_config_mode.assert_called_once() + + def test_send_configuration_default_cmd_verify(self, ssh_connection, mocker): + """send_configuration with default cmd_verify=True passes cmd_verify=True to send_config_set.""" + ssh_connection._check_connection = mocker.Mock(return_value=True) + ssh_connection._connection.send_config_set = mocker.Mock(return_value="") + commands = ["interface ethernet1/1/1", "no shutdown"] + + ssh_connection.send_configuration(commands) + + ssh_connection._connection.send_config_set.assert_called_once_with( + commands, exit_config_mode=True, enter_config_mode=True, cmd_verify=True + ) + + def test_send_configuration_cmd_verify_false(self, ssh_connection, mocker): + """send_configuration with cmd_verify=False passes cmd_verify=False to send_config_set. + + This is used for interactive commands (e.g. 'default interface ') that + produce a confirmation prompt; the confirmation response is appended to commands + and consumed by the device without per-command echo verification. + """ + ssh_connection._check_connection = mocker.Mock(return_value=True) + ssh_connection._connection.send_config_set = mocker.Mock(return_value="") + commands = ["default interface ethernet1/1/1", "yes"] + + ssh_connection.send_configuration(commands, cmd_verify=False) + + ssh_connection._connection.send_config_set.assert_called_once_with( + commands, exit_config_mode=True, enter_config_mode=True, cmd_verify=False + ) diff --git a/tests/unit/test_mfd_switchmanagement/test_vendors/test_dell/test_dell_os10/test_base.py b/tests/unit/test_mfd_switchmanagement/test_vendors/test_dell/test_dell_os10/test_base.py index 31d0b37..39631c0 100644 --- a/tests/unit/test_mfd_switchmanagement/test_vendors/test_dell/test_dell_os10/test_base.py +++ b/tests/unit/test_mfd_switchmanagement/test_vendors/test_dell/test_dell_os10/test_base.py @@ -1318,7 +1318,9 @@ def test_disable_pfc_ndk_type(self, switch, mocker): switch.delete_port_pfc.assert_called_once_with(port) switch.clear_port_dcbx.assert_called_once_with(port) switch.delete_qos_policy.assert_called_once_with(suffix="100") - switch._connection.send_configuration.assert_called_once_with([f"default interface {port}"]) + switch._connection.send_configuration.assert_called_once_with( + [f"default interface {port}", "yes"], cmd_verify=False + ) def test_disable_pfc_userspace_type(self, switch, mocker): """Test disable_pfc with userspace type.""" @@ -1340,7 +1342,9 @@ def test_disable_pfc_userspace_type(self, switch, mocker): switch.delete_port_pfc.assert_called_once_with(port) switch.clear_port_dcbx.assert_called_once_with(port) switch.delete_qos_policy_userspace.assert_called_once_with(suffix="100") - switch._connection.send_configuration.assert_called_once_with([f"default interface {port}"]) + switch._connection.send_configuration.assert_called_once_with( + [f"default interface {port}", "yes"], cmd_verify=False + ) def test_disable_pfc_custom_suffix(self, switch, mocker): """Test disable_pfc with custom suffix."""