diff --git a/PLATFORMS.md b/PLATFORMS.md index dd5b6fe29..21c84ed97 100644 --- a/PLATFORMS.md +++ b/PLATFORMS.md @@ -74,11 +74,12 @@ - A10 - Accedian -- Alaxala AX2600S and AX3600S +- Alaxala AX2600S and AX3600S - Allied Telesis AlliedWare Plus - Arris CER - Aruba OS (Wireless Controllers/WAPs) - Aruba AOS-CX +- Avara OAP800 - Bintec BOSS (Bintec/Funkwerk) - Brocade Fabric OS - C-DOT CROS @@ -259,7 +260,7 @@ - yamaha - zte_zxros - zyxel_os - + ###### Supported Telnet device_type values - adtran_os_telnet diff --git a/netmiko/avara/__init__.py b/netmiko/avara/__init__.py new file mode 100644 index 000000000..6d228c85e --- /dev/null +++ b/netmiko/avara/__init__.py @@ -0,0 +1,5 @@ +from netmiko.avara.avara import AvaraSSH + +__all__ = [ + "AvaraSSH", +] diff --git a/netmiko/avara/avara.py b/netmiko/avara/avara.py new file mode 100644 index 000000000..022b079d8 --- /dev/null +++ b/netmiko/avara/avara.py @@ -0,0 +1,216 @@ +import re +from typing import Any, Optional + +from netmiko.cisco_base_connection import CiscoSSHConnection +from netmiko.exceptions import NetmikoTimeoutException + +# Example Prompts: +# - User: `DEVNAME % ` +# - Config: `DEVNAME* % ` + +USER_PROMPT: str = " %" +CONFIG_PROMPT: str = "* %" +# The user prompt regex needs to exclude matches with the '*' +# Otherwise, it will also match the config prompt. +USER_MODE_REGEX: str = r"[^\*]\s%" +CONFIG_MODE_REGEX: str = r"\*\s%" + + +class AvaraSSH(CiscoSSHConnection): + """Avara SSH Driver for Netmiko.""" + + def session_preparation(self) -> None: + """Prepare the session after the connection has been established. + + Returns: + None + """ + self._test_channel_read(pattern=USER_MODE_REGEX) + self.base_prompt = self.find_prompt() + + def enable( + self, + cmd: str = "enable", + pattern: str = "assword", + enable_pattern: Optional[str] = None, + check_state: bool = False, + re_flags: int = re.IGNORECASE, + ) -> str: + """Enter enable mode, which is configuration mode on Avara devices. + + Args: + cmd: Command to enter enable mode + pattern: Pattern to search for password prompt + enable_pattern: Pattern to search for enable mode prompt + re_flags: Regular expression flags + + Returns: + str: Output of entering enable mode + """ + error_msg = ( + "Failed to enter enable mode. Please ensure you pass " + "the 'secret' argument to ConnectHandler." + ) + output = "" + if not self.check_config_mode(): + try: + # Send "enable" mode command + self.write_channel(self.normalize_cmd(cmd)) + + # Read the command echo + if self.global_cmd_verify is not False: + output += self.read_until_pattern(pattern=re.escape(cmd.strip())) + + # Search for trailing prompt or password pattern + output += self.read_until_prompt_or_pattern( + pattern=pattern, re_flags=re_flags + ) + + # Send the "secret" in response to password pattern + if re.search(pattern, output): + self.write_channel(self.normalize_cmd(self.secret)) + output += self.read_channel_timing(read_timeout=0) + + if not self.check_config_mode(): + raise ValueError(error_msg) + + except NetmikoTimeoutException: + raise ValueError(error_msg) + + return output + return "Config mode already enabled." + + def config_mode(self): + """Redirect config mode to enable mode""" + return self.enable() + + def exit_enable_mode(self, exit_command: str = "disable") -> str: + """ + Exit from enable mode. + Args: + exit_command: Command to exit enable mode + + Returns: + str: Output of the exit command + + Raises: + ValueError: If unable to exit enable mode + """ + return self.exit_config_mode(exit_config=exit_command) + + def check_config_mode( + self, + check_string: str = CONFIG_PROMPT, + pattern: str = "", + force_regex: bool = False, + ) -> bool: + """ + Check if the device is in configuration mode. + + Args: + check_string: String pattern to check for configuration mode. Defaults to CONFIG_PROMPT. + pattern: Additional pattern to check for configuration mode. Defaults to empty string. + force_regex: Whether to use regex for matching. Defaults to False. + + Returns: + bool: True if device is in configuration mode, False otherwise. + """ + # Silence warnings + _pattern = pattern # noqa + _force_regex = force_regex # noqa + + self.write_channel(self.RETURN) + output = self.read_channel_timing(read_timeout=0.5) + return check_string in output + + def send_config_set( + self, + config_commands: Any = None, + exit_config_mode: bool = False, + terminator: str = "%", + **kwargs: Any, + ) -> str: + """ + Send configuration commands to the device. + + Args: + config_commands: A string or list of strings containing configuration commands. + exit_config_mode: If True, exit configuration mode when complete. + **kwargs: Additional arguments to pass to send_config_set method. + + Returns: + str: The output of the configuration commands. + """ + # if not self.check_config_mode(): + # self.enable() + + return super().send_config_set( + config_commands=config_commands, + exit_config_mode=exit_config_mode, + terminator=terminator, + **kwargs, + ) + + def exit_config_mode( + self, exit_config: str = "disable", pattern: str = r"Edit mode exited\." + ) -> str: + """Exit from configuration mode. + + Args: + exit_config: Command to exit configuration mode + pattern: Pattern to terminate reading of channel + + Returns: + str: The output of the exit configuration commands. + """ + return super().exit_config_mode(exit_config=exit_config, pattern=pattern) + + def save_config( + self, cmd: str = "save flash", confirm: bool = False, confirm_response: str = "" + ) -> str: + """ + Save the configuration to flash memory. + This method first applies any pending configuration edits using the "apply" command, + and then saves the configuration to flash memory using the parent class's save_config + method. + + Args: + cmd: The command to save the configuration. Default is "save flash". + confirm: Whether confirmation is required or not. Default is False. + confirm_response: The response to confirmation prompt. Default is empty string. + + Returns: + str: The output of the save configuration command. + """ + + # Apply Edits + self.send_command(command_string="apply", expect_string="Edits applied.") + + # Save configuration to flash + return super().save_config( + cmd=cmd, + confirm=confirm, + confirm_response=confirm_response, + ) + + def disconnect(self) -> None: + """ + Attempts to gracefully close the SSH connection to the device + and the log files. Any exceptions during closure are suppressed. + + Returns: + None + """ + + try: + if self.remote_conn: + self.remote_conn.close() + if self.remote_conn_pre: + self.remote_conn_pre.close() + except Exception: + pass + finally: + self.remote_conn_pre = None + self.remote_conn = None + if self.session_log: + self.session_log.close() diff --git a/netmiko/ssh_dispatcher.py b/netmiko/ssh_dispatcher.py index 17bba09bf..10875c61f 100644 --- a/netmiko/ssh_dispatcher.py +++ b/netmiko/ssh_dispatcher.py @@ -26,7 +26,7 @@ Audiocode66Telnet, AudiocodeShellTelnet, ) - +from netmiko.avara import AvaraSSH from netmiko.bintec import BintecBossSSH, BintecBossTelnet from netmiko.brocade import BrocadeFOSSSH from netmiko.broadcom import BroadcomIcosSSH @@ -201,6 +201,7 @@ "audiocode_shell": AudiocodeShellSSH, "avaya_ers": ExtremeErsSSH, "avaya_vsp": ExtremeVspSSH, + "avara_oap": AvaraSSH, "bintec_boss": BintecBossSSH, "broadcom_icos": BroadcomIcosSSH, "brocade_fos": BrocadeFOSSSH, diff --git a/tests/etc/commands.yml.example b/tests/etc/commands.yml.example index a96d999b5..c4c2875ff 100644 --- a/tests/etc/commands.yml.example +++ b/tests/etc/commands.yml.example @@ -634,4 +634,19 @@ fs_os: config_verification: "show run | inc logging buffer" save_config_cmd: 'write' save_config_confirm: False - save_config_response: 'OK' \ No newline at end of file + save_config_response: 'OK' + +avara_oap: + version: "show version" + basic: "show status system" + extended_output: "" + config: + - config eth state disable 6 + - config eth state enable 6 + config_verification: "show edits eth 6" + enter_config_mode: False + exit_config_mode: False + support_commit: False + save_config_cmd: 'apply' + save_config_confirm: False + save_config_response: 'Edits applied.' \ No newline at end of file diff --git a/tests/etc/responses.yml.example b/tests/etc/responses.yml.example index 2fdd14862..15dfeff1d 100644 --- a/tests/etc/responses.yml.example +++ b/tests/etc/responses.yml.example @@ -472,3 +472,14 @@ fs_os: version_banner: "FS Data Center Switch" multiple_line_output: "Building configuration..." save_config: '[OK]' + +avara_oap: + base_prompt: "7D8558361 %" + router_prompt: "7D8558361 %" + enable_prompt: "7D8558361* %" + interface_ip: 192.168.1.1 + version_banner: "Version Information:" + multiple_line_output: "" + cmd_response_init: "disable" + cmd_response_final: "enable" + save_config: 'Config saved to flash.' diff --git a/tests/etc/test_devices.yml.example b/tests/etc/test_devices.yml.example index 3e1d92a57..32f355654 100644 --- a/tests/etc/test_devices.yml.example +++ b/tests/etc/test_devices.yml.example @@ -334,4 +334,11 @@ fs_os: device_type: fs_os ip: 10.1.11.200 username: admin - password: admin \ No newline at end of file + password: admin + +avara_oap: + device_type: avara_oap + ip: 192.168.1.1 + username: admin + password: admin + secret: admin \ No newline at end of file