diff --git a/.gitignore b/.gitignore index b8d9663f4..c2be9af79 100644 --- a/.gitignore +++ b/.gitignore @@ -15,9 +15,17 @@ dist netmiko.egg-info .cache bin/.netmiko.cfg + +# IDE Related files .vscode/* .idea/ + +# Virtual Environment files .venv/ +bin/ +lib/ +pyvenv.cfg +.devcontainer # Sphinx documentation docs/build/doctrees/ diff --git a/PLATFORMS.md b/PLATFORMS.md index 806f0cb6d..859a327b3 100644 --- a/PLATFORMS.md +++ b/PLATFORMS.md @@ -29,6 +29,8 @@ - Extreme MLX/NetIron (Brocade/Foundry) - HPE Comware7 - Huawei +- Huawei OLT +- Huawei SmartAX - IP Infusion OcNOS - Juniper ScreenOS - Mellanox @@ -71,5 +73,6 @@ - QuantaMesh - Rad ETX - Sophos SFOS +- Ubiquiti Unifi Switch - Versa Networks FlexVNF - Watchguard Firebox diff --git a/docs/netmiko/aruba/aruba_ssh.html b/docs/netmiko/aruba/aruba_ssh.html index 1998d22f2..a19342dd4 100644 --- a/docs/netmiko/aruba/aruba_ssh.html +++ b/docs/netmiko/aruba/aruba_ssh.html @@ -35,6 +35,9 @@

Module netmiko.aruba.aruba_ssh

def __init__(self, **kwargs): if kwargs.get("default_enter") is None: kwargs["default_enter"] = "\r" + # Aruba has an auto-complete on space behavior that is problematic + if kwargs.get("global_cmd_verify") is None: + kwargs["global_cmd_verify"] = False return super().__init__(**kwargs) def session_preparation(self): @@ -57,7 +60,13 @@

Module netmiko.aruba.aruba_ssh

""" if not pattern: pattern = re.escape(self.base_prompt[:16]) - return super().check_config_mode(check_string=check_string, pattern=pattern) + return super().check_config_mode(check_string=check_string, pattern=pattern) + + def config_mode(self, config_command="configure term", pattern=""): + """ + Aruba auto completes on space so 'configure' needs fully spelled-out. + """ + return super().config_mode(config_command=config_command, pattern=pattern)
@@ -209,6 +218,9 @@

Classes

def __init__(self, **kwargs): if kwargs.get("default_enter") is None: kwargs["default_enter"] = "\r" + # Aruba has an auto-complete on space behavior that is problematic + if kwargs.get("global_cmd_verify") is None: + kwargs["global_cmd_verify"] = False return super().__init__(**kwargs) def session_preparation(self): @@ -231,7 +243,13 @@

Classes

""" if not pattern: pattern = re.escape(self.base_prompt[:16]) - return super().check_config_mode(check_string=check_string, pattern=pattern) + return super().check_config_mode(check_string=check_string, pattern=pattern) + + def config_mode(self, config_command="configure term", pattern=""): + """ + Aruba auto completes on space so 'configure' needs fully spelled-out. + """ + return super().config_mode(config_command=config_command, pattern=pattern)

Ancestors

@@ -35,6 +36,10 @@

Sub-modules

+
netmiko.huawei.huawei_smartax
+
+
+
@@ -184,8 +189,9 @@

Classes

def special_login_handler(self): """Handle password change request by ignoring it""" - password_change_prompt = r"(Change now|Please choose 'YES' or 'NO').+" - output = self.read_until_prompt_or_pattern(password_change_prompt) + # Huawei can prompt for password change. Search for that or for normal prompt + password_change_prompt = r"((Change now|Please choose))|([\]>]\s*$)" + output = self.read_until_pattern(password_change_prompt) if re.search(password_change_prompt, output): self.write_channel("N\n") self.clear_buffer() @@ -213,8 +219,9 @@

Methods

def special_login_handler(self):
     """Handle password change request by ignoring it"""
 
-    password_change_prompt = r"(Change now|Please choose 'YES' or 'NO').+"
-    output = self.read_until_prompt_or_pattern(password_change_prompt)
+    # Huawei can prompt for password change. Search for that or for normal prompt
+    password_change_prompt = r"((Change now|Please choose))|([\]>]\s*$)"
+    output = self.read_until_pattern(password_change_prompt)
     if re.search(password_change_prompt, output):
         self.write_channel("N\n")
         self.clear_buffer()
@@ -269,6 +276,340 @@ 

Inherited members

+
+class HuaweiSmartAXSSH +(ip='', host='', username='', password=None, secret='', port=None, device_type='', verbose=False, global_delay_factor=1, global_cmd_verify=None, use_keys=False, key_file=None, pkey=None, passphrase=None, allow_agent=False, ssh_strict=False, system_host_keys=False, alt_host_keys=False, alt_key_file='', ssh_config_file=None, timeout=100, session_timeout=60, auth_timeout=None, blocking_timeout=20, banner_timeout=15, keepalive=0, default_enter=None, response_return=None, serial_settings=None, fast_cli=False, session_log=None, session_log_record_writes=False, session_log_file_mode='write', allow_auto_change=False, encoding='ascii', sock=None) +
+
+

Supports Huawei SmartAX and OLT.

+
    Initialize attributes for establishing connection to target device.
+
+    :param ip: IP address of target device. Not required if `host` is
+        provided.
+    :type ip: str
+
+    :param host: Hostname of target device. Not required if `ip` is
+            provided.
+    :type host: str
+
+    :param username: Username to authenticate against target device if
+            required.
+    :type username: str
+
+    :param password: Password to authenticate against target device if
+            required.
+    :type password: str
+
+    :param secret: The enable password if target device requires one.
+    :type secret: str
+
+    :param port: The destination port used to connect to the target
+            device.
+    :type port: int or None
+
+    :param device_type: Class selection based on device type.
+    :type device_type: str
+
+    :param verbose: Enable additional messages to standard output.
+    :type verbose: bool
+
+    :param global_delay_factor: Multiplication factor affecting Netmiko delays (default: 1).
+    :type global_delay_factor: int
+
+    :param use_keys: Connect to target device using SSH keys.
+    :type use_keys: bool
+
+    :param key_file: Filename path of the SSH key file to use.
+    :type key_file: str
+
+    :param pkey: SSH key object to use.
+    :type pkey: paramiko.PKey
+
+    :param passphrase: Passphrase to use for encrypted key; password will be used for key
+            decryption if not specified.
+    :type passphrase: str
+
+    :param allow_agent: Enable use of SSH key-agent.
+    :type allow_agent: bool
+
+    :param ssh_strict: Automatically reject unknown SSH host keys (default: False, which
+            means unknown SSH host keys will be accepted).
+    :type ssh_strict: bool
+
+    :param system_host_keys: Load host keys from the users known_hosts file.
+    :type system_host_keys: bool
+    :param alt_host_keys: If `True` host keys will be loaded from the file specified in
+            alt_key_file.
+    :type alt_host_keys: bool
+
+    :param alt_key_file: SSH host key file to use (if alt_host_keys=True).
+    :type alt_key_file: str
+
+    :param ssh_config_file: File name of OpenSSH configuration file.
+    :type ssh_config_file: str
+
+    :param timeout: Connection timeout.
+    :type timeout: float
+
+    :param session_timeout: Set a timeout for parallel requests.
+    :type session_timeout: float
+
+    :param auth_timeout: Set a timeout (in seconds) to wait for an authentication response.
+    :type auth_timeout: float
+
+    :param banner_timeout: Set a timeout to wait for the SSH banner (pass to Paramiko).
+    :type banner_timeout: float
+
+    :param keepalive: Send SSH keepalive packets at a specific interval, in seconds.
+            Currently defaults to 0, for backwards compatibility (it will not attempt
+            to keep the connection alive).
+    :type keepalive: int
+
+    :param default_enter: Character(s) to send to correspond to enter key (default:
+
+

). +:type default_enter: str

+
    :param response_return: Character(s) to use in normalized return data to represent
+            enter key (default:
+
+

) +:type response_return: str

+
    :param fast_cli: Provide a way to optimize for performance. Converts select_delay_factor
+            to select smallest of global and specific. Sets default global_delay_factor to .1
+            (default: False)
+    :type fast_cli: boolean
+
+    :param session_log: File path or BufferedIOBase subclass object to write the session log to.
+    :type session_log: str
+
+    :param session_log_record_writes: The session log generally only records channel reads due
+            to eliminate command duplication due to command echo. You can enable this if you
+            want to record both channel reads and channel writes in the log (default: False).
+    :type session_log_record_writes: boolean
+
+    :param session_log_file_mode: "write" or "append" for session_log file mode
+            (default: "write")
+    :type session_log_file_mode: str
+
+    :param allow_auto_change: Allow automatic configuration changes for terminal settings.
+            (default: False)
+    :type allow_auto_change: bool
+
+    :param encoding: Encoding to be used when writing bytes to the output channel.
+            (default: ascii)
+    :type encoding: str
+
+    :param sock: An open socket or socket-like object (such as a `.Channel`) to use for
+            communication to the target host (default: None).
+    :type sock: socket
+
+    :param global_cmd_verify: Control whether command echo verification is enabled or disabled
+            (default: None). Global attribute takes precedence over function `cmd_verify`
+            argument. Value of `None` indicates to use function `cmd_verify` argument.
+    :type global_cmd_verify: bool|None
+
+
+Source code +
class HuaweiSmartAXSSH(CiscoBaseConnection):
+    """Supports Huawei SmartAX and OLT."""
+
+    def session_preparation(self):
+        """Prepare the session after the connection has been established."""
+        self.ansi_escape_codes = True
+        self._test_channel_read()
+        self.set_base_prompt()
+        self._disable_smart_interaction()
+        self.disable_paging()
+        # Clear the read buffer
+        time.sleep(0.3 * self.global_delay_factor)
+        self.clear_buffer()
+
+    def strip_ansi_escape_codes(self, string_buffer):
+        """
+        Huawei does a strange thing where they add a space and then add ESC[1D
+        to move the cursor to the left one.
+        The extra space is problematic.
+        """
+        code_cursor_left = chr(27) + r"\[\d+D"
+        output = string_buffer
+        pattern = rf" {code_cursor_left}"
+        output = re.sub(pattern, "", output)
+
+        log.debug("Stripping ANSI escape codes")
+        log.debug(f"new_output = {output}")
+        log.debug(f"repr = {repr(output)}")
+        return super().strip_ansi_escape_codes(output)
+
+    def _disable_smart_interaction(self, command="undo smart", delay_factor=1):
+        """Disables the { <cr> } prompt to avoid having to sent a 2nd return after each command"""
+        delay_factor = self.select_delay_factor(delay_factor)
+        time.sleep(delay_factor * 0.1)
+        self.clear_buffer()
+        command = self.normalize_cmd(command)
+        log.debug("In disable_smart_interaction")
+        log.debug(f"Command: {command}")
+        self.write_channel(command)
+        if self.global_cmd_verify is not False:
+            output = self.read_until_pattern(pattern=re.escape(command.strip()))
+        else:
+            output = self.read_until_prompt()
+        log.debug(f"{output}")
+        log.debug("Exiting disable_smart_interaction")
+
+    def disable_paging(self, command="scroll"):
+        return super().disable_paging(command=command)
+
+    def config_mode(self, config_command="config", pattern=""):
+        """Enter configuration mode."""
+        return super().config_mode(config_command=config_command, pattern=pattern)
+
+    def check_config_mode(self, check_string=")#"):
+        return super().check_config_mode(check_string=check_string)
+
+    def exit_config_mode(self, exit_config="return"):
+        return super().exit_config_mode(exit_config=exit_config)
+
+    def check_enable_mode(self, check_string="#"):
+        return super().check_enable_mode(check_string=check_string)
+
+    def enable(self, cmd="enable", pattern="", re_flags=re.IGNORECASE):
+        return super().enable(cmd=cmd, pattern=pattern, re_flags=re_flags)
+
+    def set_base_prompt(self, pri_prompt_terminator=">", alt_prompt_terminator="#"):
+        return super().set_base_prompt(
+            pri_prompt_terminator=pri_prompt_terminator,
+            alt_prompt_terminator=alt_prompt_terminator,
+        )
+
+    def save_config(self, cmd="save", confirm=False, confirm_response=""):
+        """ Save Config for HuaweiSSH"""
+        return super().save_config(
+            cmd=cmd, confirm=confirm, confirm_response=confirm_response
+        )
+
+

Ancestors

+ +

Methods

+
+
+def config_mode(self, config_command='config', pattern='') +
+
+

Enter configuration mode.

+
+Source code +
def config_mode(self, config_command="config", pattern=""):
+    """Enter configuration mode."""
+    return super().config_mode(config_command=config_command, pattern=pattern)
+
+
+
+def save_config(self, cmd='save', confirm=False, confirm_response='') +
+
+

Save Config for HuaweiSSH

+
+Source code +
def save_config(self, cmd="save", confirm=False, confirm_response=""):
+    """ Save Config for HuaweiSSH"""
+    return super().save_config(
+        cmd=cmd, confirm=confirm, confirm_response=confirm_response
+    )
+
+
+
+def session_preparation(self) +
+
+

Prepare the session after the connection has been established.

+
+Source code +
def session_preparation(self):
+    """Prepare the session after the connection has been established."""
+    self.ansi_escape_codes = True
+    self._test_channel_read()
+    self.set_base_prompt()
+    self._disable_smart_interaction()
+    self.disable_paging()
+    # Clear the read buffer
+    time.sleep(0.3 * self.global_delay_factor)
+    self.clear_buffer()
+
+
+
+def strip_ansi_escape_codes(self, string_buffer) +
+
+

Huawei does a strange thing where they add a space and then add ESC[1D +to move the cursor to the left one. +The extra space is problematic.

+
+Source code +
def strip_ansi_escape_codes(self, string_buffer):
+    """
+    Huawei does a strange thing where they add a space and then add ESC[1D
+    to move the cursor to the left one.
+    The extra space is problematic.
+    """
+    code_cursor_left = chr(27) + r"\[\d+D"
+    output = string_buffer
+    pattern = rf" {code_cursor_left}"
+    output = re.sub(pattern, "", output)
+
+    log.debug("Stripping ANSI escape codes")
+    log.debug(f"new_output = {output}")
+    log.debug(f"repr = {repr(output)}")
+    return super().strip_ansi_escape_codes(output)
+
+
+
+

Inherited members

+ +
class HuaweiTelnet (ip='', host='', username='', password=None, secret='', port=None, device_type='', verbose=False, global_delay_factor=1, global_cmd_verify=None, use_keys=False, key_file=None, pkey=None, passphrase=None, allow_agent=False, ssh_strict=False, system_host_keys=False, alt_host_keys=False, alt_key_file='', ssh_config_file=None, timeout=100, session_timeout=60, auth_timeout=None, blocking_timeout=20, banner_timeout=15, keepalive=0, default_enter=None, response_return=None, serial_settings=None, fast_cli=False, session_log=None, session_log_record_writes=False, session_log_file_mode='write', allow_auto_change=False, encoding='ascii', sock=None) @@ -922,6 +1263,7 @@

Index

  • Sub-modules

  • Classes

    @@ -933,6 +1275,15 @@

    H

  • +

    HuaweiSmartAXSSH

    + +
  • +
  • HuaweiTelnet

    • telnet_login
    • diff --git a/docs/netmiko/index.html b/docs/netmiko/index.html index 36984bf0c..54220c5da 100644 --- a/docs/netmiko/index.html +++ b/docs/netmiko/index.html @@ -47,7 +47,7 @@

      Module netmiko

      # Alternate naming Netmiko = ConnectHandler -__version__ = "3.0.0" +__version__ = "3.1.1" __all__ = ( "ConnectHandler", "ssh_dispatcher", @@ -361,7 +361,7 @@

      Functions

      -def file_transfer(ssh_conn, source_file, dest_file, file_system=None, direction='put', disable_md5=False, inline_transfer=False, overwrite_file=False, socket_timeout=10.0) +def file_transfer(ssh_conn, source_file, dest_file, file_system=None, direction='put', disable_md5=False, inline_transfer=False, overwrite_file=False, socket_timeout=10.0, verify_file=None)

      Use Secure Copy or Inline (IOS-only) to transfer files to/from network devices.

      @@ -383,6 +383,7 @@

      Functions

      inline_transfer=False, overwrite_file=False, socket_timeout=10.0, + verify_file=None, ): """Use Secure Copy or Inline (IOS-only) to transfer files to/from network devices. @@ -417,6 +418,10 @@

      Functions

      if not cisco_ios and inline_transfer: raise ValueError("Inline Transfer only supported for Cisco IOS/Cisco IOS-XE") + # Replace disable_md5 argument with verify_file argument across time + if verify_file is None: + verify_file = not disable_md5 + scp_args = { "ssh_conn": ssh_conn, "source_file": source_file, @@ -432,13 +437,13 @@

      Functions

      with TransferClass(**scp_args) as scp_transfer: if scp_transfer.check_file_exists(): if overwrite_file: - if not disable_md5: - if scp_transfer.compare_md5(): + if verify_file: + if scp_transfer.verify_file(): return nottransferred_but_verified else: # File exists, you can overwrite it, MD5 is wrong (transfer file) verifyspace_and_transferfile(scp_transfer) - if scp_transfer.compare_md5(): + if scp_transfer.verify_file(): return transferred_and_verified else: raise ValueError( @@ -450,16 +455,16 @@

      Functions

      return transferred_and_notverified else: # File exists, but you can't overwrite it. - if not disable_md5: - if scp_transfer.compare_md5(): + if verify_file: + if scp_transfer.verify_file(): return nottransferred_but_verified msg = "File already exists and overwrite_file is disabled" raise ValueError(msg) else: verifyspace_and_transferfile(scp_transfer) # File doesn't exist - if not disable_md5: - if scp_transfer.compare_md5(): + if verify_file: + if scp_transfer.verify_file(): return transferred_and_verified else: raise ValueError("MD5 failure between source and destination files") @@ -1477,18 +1482,16 @@

      Classes

      output = self.strip_prompt(output) return output - def establish_connection(self, width=None, height=None): + def establish_connection(self, width=511, height=1000): """Establish SSH connection to the network device Timeout will generate a NetmikoTimeoutException Authentication failure will generate a NetmikoAuthenticationException - width and height are needed for Fortinet paging setting. - - :param width: Specified width of the VT100 terminal window + :param width: Specified width of the VT100 terminal window (default: 511) :type width: int - :param height: Specified height of the VT100 terminal window + :param height: Specified height of the VT100 terminal window (default: 1000) :type height: int """ if self.protocol == "telnet": @@ -1524,12 +1527,9 @@

      Classes

      print(f"SSH connection established to {self.host}:{self.port}") # Use invoke_shell to establish an 'interactive session' - if width and height: - self.remote_conn = self.remote_conn_pre.invoke_shell( - term="vt100", width=width, height=height - ) - else: - self.remote_conn = self.remote_conn_pre.invoke_shell() + self.remote_conn = self.remote_conn_pre.invoke_shell( + term="vt100", width=width, height=height + ) self.remote_conn.settimeout(self.blocking_timeout) if self.keepalive: @@ -1635,7 +1635,10 @@

      Classes

      log.debug(f"Command: {command}") self.write_channel(command) # Make sure you read until you detect the command echo (avoid getting out of sync) - output = self.read_until_pattern(pattern=re.escape(command.strip())) + if self.global_cmd_verify is not False: + output = self.read_until_pattern(pattern=re.escape(command.strip())) + else: + output = self.read_until_prompt() log.debug(f"{output}") log.debug("Exiting disable_paging") return output @@ -1658,7 +1661,10 @@

      Classes

      command = self.normalize_cmd(command) self.write_channel(command) # Make sure you read until you detect the command echo (avoid getting out of sync) - output = self.read_until_pattern(pattern=re.escape(command.strip())) + if self.global_cmd_verify is not False: + output = self.read_until_pattern(pattern=re.escape(command.strip())) + else: + output = self.read_until_prompt() return output def set_base_prompt( @@ -2225,7 +2231,10 @@

      Classes

      if not self.check_config_mode(): self.write_channel(self.normalize_cmd(config_command)) # Make sure you read until you detect the command echo (avoid getting out of sync) - output += self.read_until_pattern(pattern=re.escape(config_command.strip())) + if self.global_cmd_verify is not False: + output += self.read_until_pattern( + pattern=re.escape(config_command.strip()) + ) if not re.search(pattern, output, flags=re.M): output += self.read_until_pattern(pattern=pattern) if not self.check_config_mode(): @@ -2245,7 +2254,10 @@

      Classes

      if self.check_config_mode(): self.write_channel(self.normalize_cmd(exit_config)) # Make sure you read until you detect the command echo (avoid getting out of sync) - output += self.read_until_pattern(pattern=re.escape(exit_config.strip())) + if self.global_cmd_verify is not False: + output += self.read_until_pattern( + pattern=re.escape(exit_config.strip()) + ) if not re.search(pattern, output, flags=re.M): output += self.read_until_pattern(pattern=pattern) if self.check_config_mode(): @@ -2696,7 +2708,10 @@

      Methods

      if not self.check_config_mode(): self.write_channel(self.normalize_cmd(config_command)) # Make sure you read until you detect the command echo (avoid getting out of sync) - output += self.read_until_pattern(pattern=re.escape(config_command.strip())) + if self.global_cmd_verify is not False: + output += self.read_until_pattern( + pattern=re.escape(config_command.strip()) + ) if not re.search(pattern, output, flags=re.M): output += self.read_until_pattern(pattern=pattern) if not self.check_config_mode(): @@ -2732,7 +2747,10 @@

      Methods

      log.debug(f"Command: {command}") self.write_channel(command) # Make sure you read until you detect the command echo (avoid getting out of sync) - output = self.read_until_pattern(pattern=re.escape(command.strip())) + if self.global_cmd_verify is not False: + output = self.read_until_pattern(pattern=re.escape(command.strip())) + else: + output = self.read_until_prompt() log.debug(f"{output}") log.debug("Exiting disable_paging") return output
  • @@ -2810,31 +2828,28 @@

    Methods

    -def establish_connection(self, width=None, height=None) +def establish_connection(self, width=511, height=1000)

    Establish SSH connection to the network device

    Timeout will generate a NetmikoTimeoutException Authentication failure will generate a NetmikoAuthenticationException

    -

    width and height are needed for Fortinet paging setting.

    -

    :param width: Specified width of the VT100 terminal window +

    :param width: Specified width of the VT100 terminal window (default: 511) :type width: int

    -

    :param height: Specified height of the VT100 terminal window +

    :param height: Specified height of the VT100 terminal window (default: 1000) :type height: int

    Source code -
    def establish_connection(self, width=None, height=None):
    +
    def establish_connection(self, width=511, height=1000):
         """Establish SSH connection to the network device
     
         Timeout will generate a NetmikoTimeoutException
         Authentication failure will generate a NetmikoAuthenticationException
     
    -    width and height are needed for Fortinet paging setting.
    -
    -    :param width: Specified width of the VT100 terminal window
    +    :param width: Specified width of the VT100 terminal window (default: 511)
         :type width: int
     
    -    :param height: Specified height of the VT100 terminal window
    +    :param height: Specified height of the VT100 terminal window (default: 1000)
         :type height: int
         """
         if self.protocol == "telnet":
    @@ -2870,12 +2885,9 @@ 

    Methods

    print(f"SSH connection established to {self.host}:{self.port}") # Use invoke_shell to establish an 'interactive session' - if width and height: - self.remote_conn = self.remote_conn_pre.invoke_shell( - term="vt100", width=width, height=height - ) - else: - self.remote_conn = self.remote_conn_pre.invoke_shell() + self.remote_conn = self.remote_conn_pre.invoke_shell( + term="vt100", width=width, height=height + ) self.remote_conn.settimeout(self.blocking_timeout) if self.keepalive: @@ -2910,7 +2922,10 @@

    Methods

    if self.check_config_mode(): self.write_channel(self.normalize_cmd(exit_config)) # Make sure you read until you detect the command echo (avoid getting out of sync) - output += self.read_until_pattern(pattern=re.escape(exit_config.strip())) + if self.global_cmd_verify is not False: + output += self.read_until_pattern( + pattern=re.escape(exit_config.strip()) + ) if not re.search(pattern, output, flags=re.M): output += self.read_until_pattern(pattern=pattern) if self.check_config_mode(): @@ -3909,7 +3924,10 @@

    Methods

    command = self.normalize_cmd(command) self.write_channel(command) # Make sure you read until you detect the command echo (avoid getting out of sync) - output = self.read_until_pattern(pattern=re.escape(command.strip())) + if self.global_cmd_verify is not False: + output = self.read_until_pattern(pattern=re.escape(command.strip())) + else: + output = self.read_until_prompt() return output
    @@ -4834,7 +4852,45 @@

    Methods

    else: return cached_results - def _autodetect_std(self, cmd="", search_patterns=None, re_flags=re.I, priority=99): + def _autodetect_remote_version( + self, search_patterns=None, re_flags=re.IGNORECASE, priority=99 + ): + """ + Method to try auto-detect the device type, by matching a regular expression on the reported + remote version of the SSH server. + + Parameters + ---------- + search_patterns : list + A list of regular expression to look for in the reported remote SSH version + (default: None). + re_flags: re.flags, optional + Any flags from the python re module to modify the regular expression (default: re.I). + priority: int, optional + The confidence the match is right between 0 and 99 (default: 99). + """ + invalid_responses = [r"^$"] + + if not search_patterns: + return 0 + + try: + remote_version = self.connection.remote_conn.transport.remote_version + for pattern in invalid_responses: + match = re.search(pattern, remote_version, flags=re.I) + if match: + return 0 + for pattern in search_patterns: + match = re.search(pattern, remote_version, flags=re_flags) + if match: + return priority + except Exception: + return 0 + return 0 + + def _autodetect_std( + self, cmd="", search_patterns=None, re_flags=re.IGNORECASE, priority=99 + ): """ Standard method to try to auto-detect the device type. This method will be called for each device_type present in SSH_MAPPER_BASE dict ('dispatch' key). It will attempt to send a diff --git a/docs/netmiko/nokia/index.html b/docs/netmiko/nokia/index.html index 2043abd0d..fc69b2878 100644 --- a/docs/netmiko/nokia/index.html +++ b/docs/netmiko/nokia/index.html @@ -22,9 +22,9 @@

    Module netmiko.nokia

    Source code -
    from netmiko.nokia.nokia_sros_ssh import NokiaSrosSSH
    +
    from netmiko.nokia.nokia_sros_ssh import NokiaSrosSSH, NokiaSrosFileTransfer
     
    -__all__ = ["NokiaSrosSSH"]
    +__all__ = ["NokiaSrosSSH", "NokiaSrosFileTransfer"]
    @@ -43,6 +43,189 @@

    Sub-modules

    Classes

    +
    +class NokiaSrosFileTransfer +(ssh_conn, source_file, dest_file, hash_supported=False, **kwargs) +
    +
    +

    Class to manage SCP file transfer and associated SSH control channel.

    +
    +Source code +
    class NokiaSrosFileTransfer(BaseFileTransfer):
    +    def __init__(
    +        self, ssh_conn, source_file, dest_file, hash_supported=False, **kwargs
    +    ):
    +        super().__init__(
    +            ssh_conn, source_file, dest_file, hash_supported=hash_supported, **kwargs
    +        )
    +
    +    def _file_cmd_prefix(self):
    +        """
    +        Allow MD-CLI to execute file operations by using classical CLI.
    +
    +        Returns "//" if the current prompt is MD-CLI (empty string otherwise).
    +        """
    +        return "//" if "@" in self.ssh_ctl_chan.base_prompt else ""
    +
    +    def remote_space_available(self, search_pattern=r"(\d+)\s+\w+\s+free"):
    +        """Return space available on remote device."""
    +
    +        # Sample text for search_pattern.
    +        # "               3 Dir(s)               961531904 bytes free."
    +        remote_cmd = self._file_cmd_prefix() + "file dir {}".format(self.file_system)
    +        remote_output = self.ssh_ctl_chan.send_command(remote_cmd)
    +        match = re.search(search_pattern, remote_output)
    +        return int(match.group(1))
    +
    +    def check_file_exists(self, remote_cmd=""):
    +        """Check if destination file exists (returns boolean)."""
    +
    +        if self.direction == "put":
    +            if not remote_cmd:
    +                remote_cmd = self._file_cmd_prefix() + "file dir {}/{}".format(
    +                    self.file_system, self.dest_file
    +                )
    +            dest_file_name = self.dest_file.replace("\\", "/").split("/")[-1]
    +            remote_out = self.ssh_ctl_chan.send_command(remote_cmd)
    +            if "File Not Found" in remote_out:
    +                return False
    +            elif dest_file_name in remote_out:
    +                return True
    +            else:
    +                raise ValueError("Unexpected output from check_file_exists")
    +        elif self.direction == "get":
    +            return os.path.exists(self.dest_file)
    +
    +    def remote_file_size(self, remote_cmd=None, remote_file=None):
    +        """Get the file size of the remote file."""
    +
    +        if remote_file is None:
    +            if self.direction == "put":
    +                remote_file = self.dest_file
    +            elif self.direction == "get":
    +                remote_file = self.source_file
    +        if not remote_cmd:
    +            remote_cmd = self._file_cmd_prefix() + "file dir {}/{}".format(
    +                self.file_system, remote_file
    +            )
    +        remote_out = self.ssh_ctl_chan.send_command(remote_cmd)
    +
    +        if "File Not Found" in remote_out:
    +            raise IOError("Unable to find file on remote system")
    +
    +        # Parse dir output for filename. Output format is:
    +        # "10/16/2019  10:00p                6738 {filename}"
    +
    +        pattern = r"\S+\s+\S+\s+(\d+)\s+{}".format(re.escape(remote_file))
    +        match = re.search(pattern, remote_out)
    +
    +        if not match:
    +            raise ValueError("Filename entry not found in dir output")
    +
    +        file_size = int(match.group(1))
    +        return file_size
    +
    +    def verify_file(self):
    +        """Verify the file has been transferred correctly based on filesize."""
    +        if self.direction == "put":
    +            return os.stat(self.source_file).st_size == self.remote_file_size(
    +                remote_file=self.dest_file
    +            )
    +        elif self.direction == "get":
    +            return (
    +                self.remote_file_size(remote_file=self.source_file)
    +                == os.stat(self.dest_file).st_size
    +            )
    +
    +    def file_md5(self, **kwargs):
    +        raise AttributeError("SR-OS does not support an MD5-hash operation.")
    +
    +    def process_md5(self, **kwargs):
    +        raise AttributeError("SR-OS does not support an MD5-hash operation.")
    +
    +    def compare_md5(self, **kwargs):
    +        raise AttributeError("SR-OS does not support an MD5-hash operation.")
    +
    +    def remote_md5(self, **kwargs):
    +        raise AttributeError("SR-OS does not support an MD5-hash operation.")
    +
    +

    Ancestors

    + +

    Methods

    +
    +
    +def check_file_exists(self, remote_cmd='') +
    +
    +

    Check if destination file exists (returns boolean).

    +
    +Source code +
    def check_file_exists(self, remote_cmd=""):
    +    """Check if destination file exists (returns boolean)."""
    +
    +    if self.direction == "put":
    +        if not remote_cmd:
    +            remote_cmd = self._file_cmd_prefix() + "file dir {}/{}".format(
    +                self.file_system, self.dest_file
    +            )
    +        dest_file_name = self.dest_file.replace("\\", "/").split("/")[-1]
    +        remote_out = self.ssh_ctl_chan.send_command(remote_cmd)
    +        if "File Not Found" in remote_out:
    +            return False
    +        elif dest_file_name in remote_out:
    +            return True
    +        else:
    +            raise ValueError("Unexpected output from check_file_exists")
    +    elif self.direction == "get":
    +        return os.path.exists(self.dest_file)
    +
    +
    +
    +def verify_file(self) +
    +
    +

    Verify the file has been transferred correctly based on filesize.

    +
    +Source code +
    def verify_file(self):
    +    """Verify the file has been transferred correctly based on filesize."""
    +    if self.direction == "put":
    +        return os.stat(self.source_file).st_size == self.remote_file_size(
    +            remote_file=self.dest_file
    +        )
    +    elif self.direction == "get":
    +        return (
    +            self.remote_file_size(remote_file=self.source_file)
    +            == os.stat(self.dest_file).st_size
    +        )
    +
    +
    +
    +

    Inherited members

    + +
    class NokiaSrosSSH (ip='', host='', username='', password=None, secret='', port=None, device_type='', verbose=False, global_delay_factor=1, global_cmd_verify=None, use_keys=False, key_file=None, pkey=None, passphrase=None, allow_agent=False, ssh_strict=False, system_host_keys=False, alt_host_keys=False, alt_key_file='', ssh_config_file=None, timeout=100, session_timeout=60, auth_timeout=None, blocking_timeout=20, banner_timeout=15, keepalive=0, default_enter=None, response_return=None, serial_settings=None, fast_cli=False, session_log=None, session_log_record_writes=False, session_log_file_mode='write', allow_auto_change=False, encoding='ascii', sock=None) @@ -216,6 +399,8 @@

    Classes

    # "@" indicates model-driven CLI (vs Classical CLI) if "@" in self.base_prompt: self.disable_paging(command="environment more false") + # To perform file operations we need to disable paging in classical-CLI also + self.disable_paging(command="//environment no more") self.set_terminal_width(command="environment console width 512") else: self.disable_paging(command="environment no more") @@ -266,7 +451,10 @@

    Classes

    output += self._discard() cmd = "quit-config" self.write_channel(self.normalize_cmd(cmd)) - output += self.read_until_pattern(pattern=re.escape(cmd)) + if self.global_cmd_verify is not False: + output += self.read_until_pattern(pattern=re.escape(cmd)) + else: + output += self.read_until_prompt() if self.check_config_mode(): raise ValueError("Failed to exit configuration mode") return output @@ -301,16 +489,25 @@

    Classes

    log.info("Apply uncommitted changes!") cmd = "commit" self.write_channel(self.normalize_cmd(cmd)) - output += self.read_until_pattern(pattern=re.escape(cmd)) - output += self.read_until_pattern(r"@") + new_output = "" + if self.global_cmd_verify is not False: + new_output += self.read_until_pattern(pattern=re.escape(cmd)) + if "@" not in new_output: + new_output += self.read_until_pattern(r"@") + output += new_output return output def _exit_all(self): """Return to the 'root' context.""" + output = "" exit_cmd = "exit all" self.write_channel(self.normalize_cmd(exit_cmd)) # Make sure you read until you detect the command echo (avoid getting out of sync) - return self.read_until_pattern(pattern=re.escape(exit_cmd)) + if self.global_cmd_verify is not False: + output += self.read_until_pattern(pattern=re.escape(exit_cmd)) + else: + output += self.read_until_prompt() + return output def _discard(self): """Discard changes from private candidate for Nokia SR OS""" @@ -318,7 +515,9 @@

    Classes

    if "@" in self.base_prompt: cmd = "discard" self.write_channel(self.normalize_cmd(cmd)) - new_output = self.read_until_pattern(pattern=re.escape(cmd)) + new_output = "" + if self.global_cmd_verify is not False: + new_output += self.read_until_pattern(pattern=re.escape(cmd)) if "@" not in new_output: new_output += self.read_until_prompt() output += new_output @@ -415,8 +614,12 @@

    Methods

    log.info("Apply uncommitted changes!") cmd = "commit" self.write_channel(self.normalize_cmd(cmd)) - output += self.read_until_pattern(pattern=re.escape(cmd)) - output += self.read_until_pattern(r"@") + new_output = "" + if self.global_cmd_verify is not False: + new_output += self.read_until_pattern(pattern=re.escape(cmd)) + if "@" not in new_output: + new_output += self.read_until_pattern(r"@") + output += new_output return output
    @@ -468,7 +671,10 @@

    Methods

    output += self._discard() cmd = "quit-config" self.write_channel(self.normalize_cmd(cmd)) - output += self.read_until_pattern(pattern=re.escape(cmd)) + if self.global_cmd_verify is not False: + output += self.read_until_pattern(pattern=re.escape(cmd)) + else: + output += self.read_until_prompt() if self.check_config_mode(): raise ValueError("Failed to exit configuration mode") return output
    @@ -610,6 +816,13 @@

    Index

  • Classes

    • +

      NokiaSrosFileTransfer

      + +
    • +
    • NokiaSrosSSH

      • check_config_mode
      • diff --git a/docs/netmiko/nokia/nokia_sros_ssh.html b/docs/netmiko/nokia/nokia_sros_ssh.html index 34884b081..26e7604a8 100644 --- a/docs/netmiko/nokia/nokia_sros_ssh.html +++ b/docs/netmiko/nokia/nokia_sros_ssh.html @@ -31,10 +31,12 @@

        Module netmiko.nokia.nokia_sros_ssh

        # https://github.com/ktbyers/netmiko/blob/develop/LICENSE import re +import os import time from netmiko import log from netmiko.base_connection import BaseConnection +from netmiko.scp_handler import BaseFileTransfer class NokiaSrosSSH(BaseConnection): @@ -63,6 +65,8 @@

        Module netmiko.nokia.nokia_sros_ssh

        # "@" indicates model-driven CLI (vs Classical CLI) if "@" in self.base_prompt: self.disable_paging(command="environment more false") + # To perform file operations we need to disable paging in classical-CLI also + self.disable_paging(command="//environment no more") self.set_terminal_width(command="environment console width 512") else: self.disable_paging(command="environment no more") @@ -113,7 +117,10 @@

        Module netmiko.nokia.nokia_sros_ssh

        output += self._discard() cmd = "quit-config" self.write_channel(self.normalize_cmd(cmd)) - output += self.read_until_pattern(pattern=re.escape(cmd)) + if self.global_cmd_verify is not False: + output += self.read_until_pattern(pattern=re.escape(cmd)) + else: + output += self.read_until_prompt() if self.check_config_mode(): raise ValueError("Failed to exit configuration mode") return output @@ -148,16 +155,25 @@

        Module netmiko.nokia.nokia_sros_ssh

        log.info("Apply uncommitted changes!") cmd = "commit" self.write_channel(self.normalize_cmd(cmd)) - output += self.read_until_pattern(pattern=re.escape(cmd)) - output += self.read_until_pattern(r"@") + new_output = "" + if self.global_cmd_verify is not False: + new_output += self.read_until_pattern(pattern=re.escape(cmd)) + if "@" not in new_output: + new_output += self.read_until_pattern(r"@") + output += new_output return output def _exit_all(self): """Return to the 'root' context.""" + output = "" exit_cmd = "exit all" self.write_channel(self.normalize_cmd(exit_cmd)) # Make sure you read until you detect the command echo (avoid getting out of sync) - return self.read_until_pattern(pattern=re.escape(exit_cmd)) + if self.global_cmd_verify is not False: + output += self.read_until_pattern(pattern=re.escape(exit_cmd)) + else: + output += self.read_until_prompt() + return output def _discard(self): """Discard changes from private candidate for Nokia SR OS""" @@ -165,7 +181,9 @@

        Module netmiko.nokia.nokia_sros_ssh

        if "@" in self.base_prompt: cmd = "discard" self.write_channel(self.normalize_cmd(cmd)) - new_output = self.read_until_pattern(pattern=re.escape(cmd)) + new_output = "" + if self.global_cmd_verify is not False: + new_output += self.read_until_pattern(pattern=re.escape(cmd)) if "@" not in new_output: new_output += self.read_until_prompt() output += new_output @@ -191,7 +209,106 @@

        Module netmiko.nokia.nokia_sros_ssh

        pass # Always try to send final 'logout'. self._session_log_fin = True - self.write_channel(command + self.RETURN) + self.write_channel(command + self.RETURN) + + +class NokiaSrosFileTransfer(BaseFileTransfer): + def __init__( + self, ssh_conn, source_file, dest_file, hash_supported=False, **kwargs + ): + super().__init__( + ssh_conn, source_file, dest_file, hash_supported=hash_supported, **kwargs + ) + + def _file_cmd_prefix(self): + """ + Allow MD-CLI to execute file operations by using classical CLI. + + Returns "//" if the current prompt is MD-CLI (empty string otherwise). + """ + return "//" if "@" in self.ssh_ctl_chan.base_prompt else "" + + def remote_space_available(self, search_pattern=r"(\d+)\s+\w+\s+free"): + """Return space available on remote device.""" + + # Sample text for search_pattern. + # " 3 Dir(s) 961531904 bytes free." + remote_cmd = self._file_cmd_prefix() + "file dir {}".format(self.file_system) + remote_output = self.ssh_ctl_chan.send_command(remote_cmd) + match = re.search(search_pattern, remote_output) + return int(match.group(1)) + + def check_file_exists(self, remote_cmd=""): + """Check if destination file exists (returns boolean).""" + + if self.direction == "put": + if not remote_cmd: + remote_cmd = self._file_cmd_prefix() + "file dir {}/{}".format( + self.file_system, self.dest_file + ) + dest_file_name = self.dest_file.replace("\\", "/").split("/")[-1] + remote_out = self.ssh_ctl_chan.send_command(remote_cmd) + if "File Not Found" in remote_out: + return False + elif dest_file_name in remote_out: + return True + else: + raise ValueError("Unexpected output from check_file_exists") + elif self.direction == "get": + return os.path.exists(self.dest_file) + + def remote_file_size(self, remote_cmd=None, remote_file=None): + """Get the file size of the remote file.""" + + if remote_file is None: + if self.direction == "put": + remote_file = self.dest_file + elif self.direction == "get": + remote_file = self.source_file + if not remote_cmd: + remote_cmd = self._file_cmd_prefix() + "file dir {}/{}".format( + self.file_system, remote_file + ) + remote_out = self.ssh_ctl_chan.send_command(remote_cmd) + + if "File Not Found" in remote_out: + raise IOError("Unable to find file on remote system") + + # Parse dir output for filename. Output format is: + # "10/16/2019 10:00p 6738 {filename}" + + pattern = r"\S+\s+\S+\s+(\d+)\s+{}".format(re.escape(remote_file)) + match = re.search(pattern, remote_out) + + if not match: + raise ValueError("Filename entry not found in dir output") + + file_size = int(match.group(1)) + return file_size + + def verify_file(self): + """Verify the file has been transferred correctly based on filesize.""" + if self.direction == "put": + return os.stat(self.source_file).st_size == self.remote_file_size( + remote_file=self.dest_file + ) + elif self.direction == "get": + return ( + self.remote_file_size(remote_file=self.source_file) + == os.stat(self.dest_file).st_size + ) + + def file_md5(self, **kwargs): + raise AttributeError("SR-OS does not support an MD5-hash operation.") + + def process_md5(self, **kwargs): + raise AttributeError("SR-OS does not support an MD5-hash operation.") + + def compare_md5(self, **kwargs): + raise AttributeError("SR-OS does not support an MD5-hash operation.") + + def remote_md5(self, **kwargs): + raise AttributeError("SR-OS does not support an MD5-hash operation.")
  • @@ -203,6 +320,189 @@

    Module netmiko.nokia.nokia_sros_ssh

    Classes

    +
    +class NokiaSrosFileTransfer +(ssh_conn, source_file, dest_file, hash_supported=False, **kwargs) +
    +
    +

    Class to manage SCP file transfer and associated SSH control channel.

    +
    +Source code +
    class NokiaSrosFileTransfer(BaseFileTransfer):
    +    def __init__(
    +        self, ssh_conn, source_file, dest_file, hash_supported=False, **kwargs
    +    ):
    +        super().__init__(
    +            ssh_conn, source_file, dest_file, hash_supported=hash_supported, **kwargs
    +        )
    +
    +    def _file_cmd_prefix(self):
    +        """
    +        Allow MD-CLI to execute file operations by using classical CLI.
    +
    +        Returns "//" if the current prompt is MD-CLI (empty string otherwise).
    +        """
    +        return "//" if "@" in self.ssh_ctl_chan.base_prompt else ""
    +
    +    def remote_space_available(self, search_pattern=r"(\d+)\s+\w+\s+free"):
    +        """Return space available on remote device."""
    +
    +        # Sample text for search_pattern.
    +        # "               3 Dir(s)               961531904 bytes free."
    +        remote_cmd = self._file_cmd_prefix() + "file dir {}".format(self.file_system)
    +        remote_output = self.ssh_ctl_chan.send_command(remote_cmd)
    +        match = re.search(search_pattern, remote_output)
    +        return int(match.group(1))
    +
    +    def check_file_exists(self, remote_cmd=""):
    +        """Check if destination file exists (returns boolean)."""
    +
    +        if self.direction == "put":
    +            if not remote_cmd:
    +                remote_cmd = self._file_cmd_prefix() + "file dir {}/{}".format(
    +                    self.file_system, self.dest_file
    +                )
    +            dest_file_name = self.dest_file.replace("\\", "/").split("/")[-1]
    +            remote_out = self.ssh_ctl_chan.send_command(remote_cmd)
    +            if "File Not Found" in remote_out:
    +                return False
    +            elif dest_file_name in remote_out:
    +                return True
    +            else:
    +                raise ValueError("Unexpected output from check_file_exists")
    +        elif self.direction == "get":
    +            return os.path.exists(self.dest_file)
    +
    +    def remote_file_size(self, remote_cmd=None, remote_file=None):
    +        """Get the file size of the remote file."""
    +
    +        if remote_file is None:
    +            if self.direction == "put":
    +                remote_file = self.dest_file
    +            elif self.direction == "get":
    +                remote_file = self.source_file
    +        if not remote_cmd:
    +            remote_cmd = self._file_cmd_prefix() + "file dir {}/{}".format(
    +                self.file_system, remote_file
    +            )
    +        remote_out = self.ssh_ctl_chan.send_command(remote_cmd)
    +
    +        if "File Not Found" in remote_out:
    +            raise IOError("Unable to find file on remote system")
    +
    +        # Parse dir output for filename. Output format is:
    +        # "10/16/2019  10:00p                6738 {filename}"
    +
    +        pattern = r"\S+\s+\S+\s+(\d+)\s+{}".format(re.escape(remote_file))
    +        match = re.search(pattern, remote_out)
    +
    +        if not match:
    +            raise ValueError("Filename entry not found in dir output")
    +
    +        file_size = int(match.group(1))
    +        return file_size
    +
    +    def verify_file(self):
    +        """Verify the file has been transferred correctly based on filesize."""
    +        if self.direction == "put":
    +            return os.stat(self.source_file).st_size == self.remote_file_size(
    +                remote_file=self.dest_file
    +            )
    +        elif self.direction == "get":
    +            return (
    +                self.remote_file_size(remote_file=self.source_file)
    +                == os.stat(self.dest_file).st_size
    +            )
    +
    +    def file_md5(self, **kwargs):
    +        raise AttributeError("SR-OS does not support an MD5-hash operation.")
    +
    +    def process_md5(self, **kwargs):
    +        raise AttributeError("SR-OS does not support an MD5-hash operation.")
    +
    +    def compare_md5(self, **kwargs):
    +        raise AttributeError("SR-OS does not support an MD5-hash operation.")
    +
    +    def remote_md5(self, **kwargs):
    +        raise AttributeError("SR-OS does not support an MD5-hash operation.")
    +
    +

    Ancestors

    + +

    Methods

    +
    +
    +def check_file_exists(self, remote_cmd='') +
    +
    +

    Check if destination file exists (returns boolean).

    +
    +Source code +
    def check_file_exists(self, remote_cmd=""):
    +    """Check if destination file exists (returns boolean)."""
    +
    +    if self.direction == "put":
    +        if not remote_cmd:
    +            remote_cmd = self._file_cmd_prefix() + "file dir {}/{}".format(
    +                self.file_system, self.dest_file
    +            )
    +        dest_file_name = self.dest_file.replace("\\", "/").split("/")[-1]
    +        remote_out = self.ssh_ctl_chan.send_command(remote_cmd)
    +        if "File Not Found" in remote_out:
    +            return False
    +        elif dest_file_name in remote_out:
    +            return True
    +        else:
    +            raise ValueError("Unexpected output from check_file_exists")
    +    elif self.direction == "get":
    +        return os.path.exists(self.dest_file)
    +
    +
    +
    +def verify_file(self) +
    +
    +

    Verify the file has been transferred correctly based on filesize.

    +
    +Source code +
    def verify_file(self):
    +    """Verify the file has been transferred correctly based on filesize."""
    +    if self.direction == "put":
    +        return os.stat(self.source_file).st_size == self.remote_file_size(
    +            remote_file=self.dest_file
    +        )
    +    elif self.direction == "get":
    +        return (
    +            self.remote_file_size(remote_file=self.source_file)
    +            == os.stat(self.dest_file).st_size
    +        )
    +
    +
    +
    +

    Inherited members

    + +
    class NokiaSrosSSH (ip='', host='', username='', password=None, secret='', port=None, device_type='', verbose=False, global_delay_factor=1, global_cmd_verify=None, use_keys=False, key_file=None, pkey=None, passphrase=None, allow_agent=False, ssh_strict=False, system_host_keys=False, alt_host_keys=False, alt_key_file='', ssh_config_file=None, timeout=100, session_timeout=60, auth_timeout=None, blocking_timeout=20, banner_timeout=15, keepalive=0, default_enter=None, response_return=None, serial_settings=None, fast_cli=False, session_log=None, session_log_record_writes=False, session_log_file_mode='write', allow_auto_change=False, encoding='ascii', sock=None) @@ -376,6 +676,8 @@

    Classes

    # "@" indicates model-driven CLI (vs Classical CLI) if "@" in self.base_prompt: self.disable_paging(command="environment more false") + # To perform file operations we need to disable paging in classical-CLI also + self.disable_paging(command="//environment no more") self.set_terminal_width(command="environment console width 512") else: self.disable_paging(command="environment no more") @@ -426,7 +728,10 @@

    Classes

    output += self._discard() cmd = "quit-config" self.write_channel(self.normalize_cmd(cmd)) - output += self.read_until_pattern(pattern=re.escape(cmd)) + if self.global_cmd_verify is not False: + output += self.read_until_pattern(pattern=re.escape(cmd)) + else: + output += self.read_until_prompt() if self.check_config_mode(): raise ValueError("Failed to exit configuration mode") return output @@ -461,16 +766,25 @@

    Classes

    log.info("Apply uncommitted changes!") cmd = "commit" self.write_channel(self.normalize_cmd(cmd)) - output += self.read_until_pattern(pattern=re.escape(cmd)) - output += self.read_until_pattern(r"@") + new_output = "" + if self.global_cmd_verify is not False: + new_output += self.read_until_pattern(pattern=re.escape(cmd)) + if "@" not in new_output: + new_output += self.read_until_pattern(r"@") + output += new_output return output def _exit_all(self): """Return to the 'root' context.""" + output = "" exit_cmd = "exit all" self.write_channel(self.normalize_cmd(exit_cmd)) # Make sure you read until you detect the command echo (avoid getting out of sync) - return self.read_until_pattern(pattern=re.escape(exit_cmd)) + if self.global_cmd_verify is not False: + output += self.read_until_pattern(pattern=re.escape(exit_cmd)) + else: + output += self.read_until_prompt() + return output def _discard(self): """Discard changes from private candidate for Nokia SR OS""" @@ -478,7 +792,9 @@

    Classes

    if "@" in self.base_prompt: cmd = "discard" self.write_channel(self.normalize_cmd(cmd)) - new_output = self.read_until_pattern(pattern=re.escape(cmd)) + new_output = "" + if self.global_cmd_verify is not False: + new_output += self.read_until_pattern(pattern=re.escape(cmd)) if "@" not in new_output: new_output += self.read_until_prompt() output += new_output @@ -575,8 +891,12 @@

    Methods

    log.info("Apply uncommitted changes!") cmd = "commit" self.write_channel(self.normalize_cmd(cmd)) - output += self.read_until_pattern(pattern=re.escape(cmd)) - output += self.read_until_pattern(r"@") + new_output = "" + if self.global_cmd_verify is not False: + new_output += self.read_until_pattern(pattern=re.escape(cmd)) + if "@" not in new_output: + new_output += self.read_until_pattern(r"@") + output += new_output return output
    @@ -628,7 +948,10 @@

    Methods

    output += self._discard() cmd = "quit-config" self.write_channel(self.normalize_cmd(cmd)) - output += self.read_until_pattern(pattern=re.escape(cmd)) + if self.global_cmd_verify is not False: + output += self.read_until_pattern(pattern=re.escape(cmd)) + else: + output += self.read_until_prompt() if self.check_config_mode(): raise ValueError("Failed to exit configuration mode") return output @@ -765,6 +1088,13 @@

    Index

  • Classes

    • +

      NokiaSrosFileTransfer

      + +
    • +
    • NokiaSrosSSH

      • check_config_mode
      • diff --git a/docs/netmiko/scp_functions.html b/docs/netmiko/scp_functions.html index 58f45a69f..b85b6d7bf 100644 --- a/docs/netmiko/scp_functions.html +++ b/docs/netmiko/scp_functions.html @@ -55,6 +55,7 @@

        Module netmiko.scp_functions

        inline_transfer=False, overwrite_file=False, socket_timeout=10.0, + verify_file=None, ): """Use Secure Copy or Inline (IOS-only) to transfer files to/from network devices. @@ -89,6 +90,10 @@

        Module netmiko.scp_functions

        if not cisco_ios and inline_transfer: raise ValueError("Inline Transfer only supported for Cisco IOS/Cisco IOS-XE") + # Replace disable_md5 argument with verify_file argument across time + if verify_file is None: + verify_file = not disable_md5 + scp_args = { "ssh_conn": ssh_conn, "source_file": source_file, @@ -104,13 +109,13 @@

        Module netmiko.scp_functions

        with TransferClass(**scp_args) as scp_transfer: if scp_transfer.check_file_exists(): if overwrite_file: - if not disable_md5: - if scp_transfer.compare_md5(): + if verify_file: + if scp_transfer.verify_file(): return nottransferred_but_verified else: # File exists, you can overwrite it, MD5 is wrong (transfer file) verifyspace_and_transferfile(scp_transfer) - if scp_transfer.compare_md5(): + if scp_transfer.verify_file(): return transferred_and_verified else: raise ValueError( @@ -122,16 +127,16 @@

        Module netmiko.scp_functions

        return transferred_and_notverified else: # File exists, but you can't overwrite it. - if not disable_md5: - if scp_transfer.compare_md5(): + if verify_file: + if scp_transfer.verify_file(): return nottransferred_but_verified msg = "File already exists and overwrite_file is disabled" raise ValueError(msg) else: verifyspace_and_transferfile(scp_transfer) # File doesn't exist - if not disable_md5: - if scp_transfer.compare_md5(): + if verify_file: + if scp_transfer.verify_file(): return transferred_and_verified else: raise ValueError("MD5 failure between source and destination files") @@ -147,7 +152,7 @@

        Module netmiko.scp_functions

        Functions

        -def file_transfer(ssh_conn, source_file, dest_file, file_system=None, direction='put', disable_md5=False, inline_transfer=False, overwrite_file=False, socket_timeout=10.0) +def file_transfer(ssh_conn, source_file, dest_file, file_system=None, direction='put', disable_md5=False, inline_transfer=False, overwrite_file=False, socket_timeout=10.0, verify_file=None)

        Use Secure Copy or Inline (IOS-only) to transfer files to/from network devices.

        @@ -169,6 +174,7 @@

        Functions

        inline_transfer=False, overwrite_file=False, socket_timeout=10.0, + verify_file=None, ): """Use Secure Copy or Inline (IOS-only) to transfer files to/from network devices. @@ -203,6 +209,10 @@

        Functions

        if not cisco_ios and inline_transfer: raise ValueError("Inline Transfer only supported for Cisco IOS/Cisco IOS-XE") + # Replace disable_md5 argument with verify_file argument across time + if verify_file is None: + verify_file = not disable_md5 + scp_args = { "ssh_conn": ssh_conn, "source_file": source_file, @@ -218,13 +228,13 @@

        Functions

        with TransferClass(**scp_args) as scp_transfer: if scp_transfer.check_file_exists(): if overwrite_file: - if not disable_md5: - if scp_transfer.compare_md5(): + if verify_file: + if scp_transfer.verify_file(): return nottransferred_but_verified else: # File exists, you can overwrite it, MD5 is wrong (transfer file) verifyspace_and_transferfile(scp_transfer) - if scp_transfer.compare_md5(): + if scp_transfer.verify_file(): return transferred_and_verified else: raise ValueError( @@ -236,16 +246,16 @@

        Functions

        return transferred_and_notverified else: # File exists, but you can't overwrite it. - if not disable_md5: - if scp_transfer.compare_md5(): + if verify_file: + if scp_transfer.verify_file(): return nottransferred_but_verified msg = "File already exists and overwrite_file is disabled" raise ValueError(msg) else: verifyspace_and_transferfile(scp_transfer) # File doesn't exist - if not disable_md5: - if scp_transfer.compare_md5(): + if verify_file: + if scp_transfer.verify_file(): return transferred_and_verified else: raise ValueError("MD5 failure between source and destination files") diff --git a/docs/netmiko/scp_handler.html b/docs/netmiko/scp_handler.html index 20604c85b..bb2d1d044 100644 --- a/docs/netmiko/scp_handler.html +++ b/docs/netmiko/scp_handler.html @@ -92,6 +92,7 @@

        Module netmiko.scp_handler

        file_system=None, direction="put", socket_timeout=10.0, + hash_supported=True, ): self.ssh_ctl_chan = ssh_conn self.source_file = source_file @@ -113,10 +114,12 @@

        Module netmiko.scp_handler

        self.file_system = file_system if direction == "put": - self.source_md5 = self.file_md5(source_file) + self.source_md5 = self.file_md5(source_file) if hash_supported else None self.file_size = os.stat(source_file).st_size elif direction == "get": - self.source_md5 = self.remote_md5(remote_file=source_file) + self.source_md5 = ( + self.remote_md5(remote_file=source_file) if hash_supported else None + ) self.file_size = self.remote_file_size(remote_file=source_file) else: raise ValueError("Invalid direction specified") @@ -418,7 +421,7 @@

        Classes

        class BaseFileTransfer -(ssh_conn, source_file, dest_file, file_system=None, direction='put', socket_timeout=10.0) +(ssh_conn, source_file, dest_file, file_system=None, direction='put', socket_timeout=10.0, hash_supported=True)

        Class to manage SCP file transfer and associated SSH control channel.

        @@ -435,6 +438,7 @@

        Classes

        file_system=None, direction="put", socket_timeout=10.0, + hash_supported=True, ): self.ssh_ctl_chan = ssh_conn self.source_file = source_file @@ -456,10 +460,12 @@

        Classes

        self.file_system = file_system if direction == "put": - self.source_md5 = self.file_md5(source_file) + self.source_md5 = self.file_md5(source_file) if hash_supported else None self.file_size = os.stat(source_file).st_size elif direction == "get": - self.source_md5 = self.remote_md5(remote_file=source_file) + self.source_md5 = ( + self.remote_md5(remote_file=source_file) if hash_supported else None + ) self.file_size = self.remote_file_size(remote_file=source_file) else: raise ValueError("Invalid direction specified") @@ -755,6 +761,7 @@

        Subclasses

      • CienaSaosFileTransfer
      • DellOS10FileTransfer
      • JuniperFileTransfer
      • +
      • NokiaSrosFileTransfer

      Static methods

      diff --git a/docs/netmiko/ssh_autodetect.html b/docs/netmiko/ssh_autodetect.html index 32db55db6..593dbfc73 100644 --- a/docs/netmiko/ssh_autodetect.html +++ b/docs/netmiko/ssh_autodetect.html @@ -166,12 +166,27 @@

      Netmiko connection creation section "priority": 99, "dispatch": "_autodetect_std", }, + "dell_os9": { + "cmd": "show system", + "search_patterns": [ + r"Dell Application Software Version: 9", + r"Dell Networking OS Version : 9", + ], + "priority": 99, + "dispatch": "_autodetect_std", + }, "dell_os10": { "cmd": "show version", "search_patterns": [r"Dell EMC Networking OS10-Enterprise"], "priority": 99, "dispatch": "_autodetect_std", }, + "dell_powerconnect": { + "cmd": "show system", + "search_patterns": [r"PowerConnect"], + "priority": 99, + "dispatch": "_autodetect_std", + }, "f5_tmsh": { "cmd": "show sys version", "search_patterns": [r"BIG-IP"], @@ -228,6 +243,17 @@

      Netmiko connection creation section "priority": 99, "dispatch": "_autodetect_std", }, + "cisco_wlc": { + "dispatch": "_autodetect_remote_version", + "search_patterns": [r"CISCO_WLC"], + "priority": 99, + }, + "mellanox_mlnxos": { + "cmd": "show version", + "search_patterns": [r"Onyx", r"SX_PPC_M460EX"], + "priority": 99, + "dispatch": "_autodetect_std", + }, } @@ -349,7 +375,45 @@

      Netmiko connection creation section else: return cached_results - def _autodetect_std(self, cmd="", search_patterns=None, re_flags=re.I, priority=99): + def _autodetect_remote_version( + self, search_patterns=None, re_flags=re.IGNORECASE, priority=99 + ): + """ + Method to try auto-detect the device type, by matching a regular expression on the reported + remote version of the SSH server. + + Parameters + ---------- + search_patterns : list + A list of regular expression to look for in the reported remote SSH version + (default: None). + re_flags: re.flags, optional + Any flags from the python re module to modify the regular expression (default: re.I). + priority: int, optional + The confidence the match is right between 0 and 99 (default: 99). + """ + invalid_responses = [r"^$"] + + if not search_patterns: + return 0 + + try: + remote_version = self.connection.remote_conn.transport.remote_version + for pattern in invalid_responses: + match = re.search(pattern, remote_version, flags=re.I) + if match: + return 0 + for pattern in search_patterns: + match = re.search(pattern, remote_version, flags=re_flags) + if match: + return priority + except Exception: + return 0 + return 0 + + def _autodetect_std( + self, cmd="", search_patterns=None, re_flags=re.IGNORECASE, priority=99 + ): """ Standard method to try to auto-detect the device type. This method will be called for each device_type present in SSH_MAPPER_BASE dict ('dispatch' key). It will attempt to send a @@ -550,7 +614,45 @@

      Methods

      else: return cached_results - def _autodetect_std(self, cmd="", search_patterns=None, re_flags=re.I, priority=99): + def _autodetect_remote_version( + self, search_patterns=None, re_flags=re.IGNORECASE, priority=99 + ): + """ + Method to try auto-detect the device type, by matching a regular expression on the reported + remote version of the SSH server. + + Parameters + ---------- + search_patterns : list + A list of regular expression to look for in the reported remote SSH version + (default: None). + re_flags: re.flags, optional + Any flags from the python re module to modify the regular expression (default: re.I). + priority: int, optional + The confidence the match is right between 0 and 99 (default: 99). + """ + invalid_responses = [r"^$"] + + if not search_patterns: + return 0 + + try: + remote_version = self.connection.remote_conn.transport.remote_version + for pattern in invalid_responses: + match = re.search(pattern, remote_version, flags=re.I) + if match: + return 0 + for pattern in search_patterns: + match = re.search(pattern, remote_version, flags=re_flags) + if match: + return priority + except Exception: + return 0 + return 0 + + def _autodetect_std( + self, cmd="", search_patterns=None, re_flags=re.IGNORECASE, priority=99 + ): """ Standard method to try to auto-detect the device type. This method will be called for each device_type present in SSH_MAPPER_BASE dict ('dispatch' key). It will attempt to send a diff --git a/docs/netmiko/ubiquiti/edge_ssh.html b/docs/netmiko/ubiquiti/edge_ssh.html index a36cd4019..66c4ae32d 100644 --- a/docs/netmiko/ubiquiti/edge_ssh.html +++ b/docs/netmiko/ubiquiti/edge_ssh.html @@ -264,6 +264,10 @@

      Ancestors

    • CiscoBaseConnection
    • BaseConnection
    +

    Subclasses

    +

    Methods

    diff --git a/docs/netmiko/ubiquiti/index.html b/docs/netmiko/ubiquiti/index.html index fe2222131..c992b98c8 100644 --- a/docs/netmiko/ubiquiti/index.html +++ b/docs/netmiko/ubiquiti/index.html @@ -23,8 +23,9 @@

    Module netmiko.ubiquiti

    Source code
    from netmiko.ubiquiti.edge_ssh import UbiquitiEdgeSSH
    +from netmiko.ubiquiti.unifiswitch_ssh import UbiquitiUnifiSwitchSSH
     
    -__all__ = ["UbiquitiEdgeSSH"]
    +__all__ = ["UbiquitiEdgeSSH", "UnifiSwitchSSH", "UbiquitiUnifiSwitchSSH"]
  • @@ -34,6 +35,10 @@

    Sub-modules

    +
    netmiko.ubiquiti.unifiswitch_ssh
    +
    +
    +
    @@ -228,6 +233,10 @@

    Ancestors

  • CiscoBaseConnection
  • BaseConnection
  • +

    Subclasses

    +

    Methods

    @@ -336,6 +345,270 @@

    Inherited members

    +
    +class UbiquitiUnifiSwitchSSH +(ip='', host='', username='', password=None, secret='', port=None, device_type='', verbose=False, global_delay_factor=1, global_cmd_verify=None, use_keys=False, key_file=None, pkey=None, passphrase=None, allow_agent=False, ssh_strict=False, system_host_keys=False, alt_host_keys=False, alt_key_file='', ssh_config_file=None, timeout=100, session_timeout=60, auth_timeout=None, blocking_timeout=20, banner_timeout=15, keepalive=0, default_enter=None, response_return=None, serial_settings=None, fast_cli=False, session_log=None, session_log_record_writes=False, session_log_file_mode='write', allow_auto_change=False, encoding='ascii', sock=None) +
    +
    +

    Implements support for Ubiquity EdgeSwitch devices.

    +

    Mostly conforms to Cisco IOS style syntax with a few minor changes.

    +

    This is NOT for EdgeRouter devices.

    +
        Initialize attributes for establishing connection to target device.
    +
    +    :param ip: IP address of target device. Not required if `host` is
    +        provided.
    +    :type ip: str
    +
    +    :param host: Hostname of target device. Not required if `ip` is
    +            provided.
    +    :type host: str
    +
    +    :param username: Username to authenticate against target device if
    +            required.
    +    :type username: str
    +
    +    :param password: Password to authenticate against target device if
    +            required.
    +    :type password: str
    +
    +    :param secret: The enable password if target device requires one.
    +    :type secret: str
    +
    +    :param port: The destination port used to connect to the target
    +            device.
    +    :type port: int or None
    +
    +    :param device_type: Class selection based on device type.
    +    :type device_type: str
    +
    +    :param verbose: Enable additional messages to standard output.
    +    :type verbose: bool
    +
    +    :param global_delay_factor: Multiplication factor affecting Netmiko delays (default: 1).
    +    :type global_delay_factor: int
    +
    +    :param use_keys: Connect to target device using SSH keys.
    +    :type use_keys: bool
    +
    +    :param key_file: Filename path of the SSH key file to use.
    +    :type key_file: str
    +
    +    :param pkey: SSH key object to use.
    +    :type pkey: paramiko.PKey
    +
    +    :param passphrase: Passphrase to use for encrypted key; password will be used for key
    +            decryption if not specified.
    +    :type passphrase: str
    +
    +    :param allow_agent: Enable use of SSH key-agent.
    +    :type allow_agent: bool
    +
    +    :param ssh_strict: Automatically reject unknown SSH host keys (default: False, which
    +            means unknown SSH host keys will be accepted).
    +    :type ssh_strict: bool
    +
    +    :param system_host_keys: Load host keys from the users known_hosts file.
    +    :type system_host_keys: bool
    +    :param alt_host_keys: If `True` host keys will be loaded from the file specified in
    +            alt_key_file.
    +    :type alt_host_keys: bool
    +
    +    :param alt_key_file: SSH host key file to use (if alt_host_keys=True).
    +    :type alt_key_file: str
    +
    +    :param ssh_config_file: File name of OpenSSH configuration file.
    +    :type ssh_config_file: str
    +
    +    :param timeout: Connection timeout.
    +    :type timeout: float
    +
    +    :param session_timeout: Set a timeout for parallel requests.
    +    :type session_timeout: float
    +
    +    :param auth_timeout: Set a timeout (in seconds) to wait for an authentication response.
    +    :type auth_timeout: float
    +
    +    :param banner_timeout: Set a timeout to wait for the SSH banner (pass to Paramiko).
    +    :type banner_timeout: float
    +
    +    :param keepalive: Send SSH keepalive packets at a specific interval, in seconds.
    +            Currently defaults to 0, for backwards compatibility (it will not attempt
    +            to keep the connection alive).
    +    :type keepalive: int
    +
    +    :param default_enter: Character(s) to send to correspond to enter key (default:
    +
    +

    ). +:type default_enter: str

    +
        :param response_return: Character(s) to use in normalized return data to represent
    +            enter key (default:
    +
    +

    ) +:type response_return: str

    +
        :param fast_cli: Provide a way to optimize for performance. Converts select_delay_factor
    +            to select smallest of global and specific. Sets default global_delay_factor to .1
    +            (default: False)
    +    :type fast_cli: boolean
    +
    +    :param session_log: File path or BufferedIOBase subclass object to write the session log to.
    +    :type session_log: str
    +
    +    :param session_log_record_writes: The session log generally only records channel reads due
    +            to eliminate command duplication due to command echo. You can enable this if you
    +            want to record both channel reads and channel writes in the log (default: False).
    +    :type session_log_record_writes: boolean
    +
    +    :param session_log_file_mode: "write" or "append" for session_log file mode
    +            (default: "write")
    +    :type session_log_file_mode: str
    +
    +    :param allow_auto_change: Allow automatic configuration changes for terminal settings.
    +            (default: False)
    +    :type allow_auto_change: bool
    +
    +    :param encoding: Encoding to be used when writing bytes to the output channel.
    +            (default: ascii)
    +    :type encoding: str
    +
    +    :param sock: An open socket or socket-like object (such as a `.Channel`) to use for
    +            communication to the target host (default: None).
    +    :type sock: socket
    +
    +    :param global_cmd_verify: Control whether command echo verification is enabled or disabled
    +            (default: None). Global attribute takes precedence over function `cmd_verify`
    +            argument. Value of `None` indicates to use function `cmd_verify` argument.
    +    :type global_cmd_verify: bool|None
    +
    +
    +Source code +
    class UbiquitiUnifiSwitchSSH(UbiquitiEdgeSSH):
    +    def session_preparation(self):
    +        """
    +        Prepare the session after the connection has been established.
    +        When SSHing to a UniFi switch, the session initially starts at a Linux
    +        shell. Nothing interesting can be done in this environment, however,
    +        running `telnet localhost` drops the session to a more familiar
    +        environment.
    +        """
    +
    +        self._test_channel_read()
    +        self.set_base_prompt()
    +        self.send_command(
    +            command_string="telnet localhost", expect_string=r"\(UBNT\) >"
    +        )
    +        self.set_base_prompt()
    +        self.enable()
    +        self.disable_paging()
    +
    +        # Clear read buffer
    +        time.sleep(0.3 * self.global_delay_factor)
    +        self.clear_buffer()
    +
    +    def cleanup(self, command="exit"):
    +        """Gracefully exit the SSH session."""
    +        try:
    +            # The pattern="" forces use of send_command_timing
    +            if self.check_config_mode(pattern=""):
    +                self.exit_config_mode()
    +
    +            # Exit from the first 'telnet localhost'
    +            self.write_channel(command + self.RETURN)
    +        except Exception:
    +            pass
    +
    +        super().cleanup()
    +
    +

    Ancestors

    + +

    Methods

    +
    +
    +def session_preparation(self) +
    +
    +

    Prepare the session after the connection has been established. +When SSHing to a UniFi switch, the session initially starts at a Linux +shell. Nothing interesting can be done in this environment, however, +running telnet localhost drops the session to a more familiar +environment.

    +
    +Source code +
    def session_preparation(self):
    +    """
    +    Prepare the session after the connection has been established.
    +    When SSHing to a UniFi switch, the session initially starts at a Linux
    +    shell. Nothing interesting can be done in this environment, however,
    +    running `telnet localhost` drops the session to a more familiar
    +    environment.
    +    """
    +
    +    self._test_channel_read()
    +    self.set_base_prompt()
    +    self.send_command(
    +        command_string="telnet localhost", expect_string=r"\(UBNT\) >"
    +    )
    +    self.set_base_prompt()
    +    self.enable()
    +    self.disable_paging()
    +
    +    # Clear read buffer
    +    time.sleep(0.3 * self.global_delay_factor)
    +    self.clear_buffer()
    +
    +
    +
    +

    Inherited members

    + +
    @@ -353,6 +626,7 @@

    Index

  • Sub-modules

  • Classes

    @@ -367,6 +641,12 @@

    save_config

  • +
  • +

    UbiquitiUnifiSwitchSSH

    + +
  • diff --git a/docs/netmiko/ubiquiti/unifiswitch_ssh.html b/docs/netmiko/ubiquiti/unifiswitch_ssh.html new file mode 100644 index 000000000..46d4db64f --- /dev/null +++ b/docs/netmiko/ubiquiti/unifiswitch_ssh.html @@ -0,0 +1,373 @@ + + + + + + +netmiko.ubiquiti.unifiswitch_ssh API documentation + + + + + + + + + +
    +
    +
    +

    Module netmiko.ubiquiti.unifiswitch_ssh

    +
    +
    +
    +Source code +
    import time
    +from netmiko.ubiquiti.edge_ssh import UbiquitiEdgeSSH
    +
    +
    +class UbiquitiUnifiSwitchSSH(UbiquitiEdgeSSH):
    +    def session_preparation(self):
    +        """
    +        Prepare the session after the connection has been established.
    +        When SSHing to a UniFi switch, the session initially starts at a Linux
    +        shell. Nothing interesting can be done in this environment, however,
    +        running `telnet localhost` drops the session to a more familiar
    +        environment.
    +        """
    +
    +        self._test_channel_read()
    +        self.set_base_prompt()
    +        self.send_command(
    +            command_string="telnet localhost", expect_string=r"\(UBNT\) >"
    +        )
    +        self.set_base_prompt()
    +        self.enable()
    +        self.disable_paging()
    +
    +        # Clear read buffer
    +        time.sleep(0.3 * self.global_delay_factor)
    +        self.clear_buffer()
    +
    +    def cleanup(self, command="exit"):
    +        """Gracefully exit the SSH session."""
    +        try:
    +            # The pattern="" forces use of send_command_timing
    +            if self.check_config_mode(pattern=""):
    +                self.exit_config_mode()
    +
    +            # Exit from the first 'telnet localhost'
    +            self.write_channel(command + self.RETURN)
    +        except Exception:
    +            pass
    +
    +        super().cleanup()
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    Classes

    +
    +
    +class UbiquitiUnifiSwitchSSH +(ip='', host='', username='', password=None, secret='', port=None, device_type='', verbose=False, global_delay_factor=1, global_cmd_verify=None, use_keys=False, key_file=None, pkey=None, passphrase=None, allow_agent=False, ssh_strict=False, system_host_keys=False, alt_host_keys=False, alt_key_file='', ssh_config_file=None, timeout=100, session_timeout=60, auth_timeout=None, blocking_timeout=20, banner_timeout=15, keepalive=0, default_enter=None, response_return=None, serial_settings=None, fast_cli=False, session_log=None, session_log_record_writes=False, session_log_file_mode='write', allow_auto_change=False, encoding='ascii', sock=None) +
    +
    +

    Implements support for Ubiquity EdgeSwitch devices.

    +

    Mostly conforms to Cisco IOS style syntax with a few minor changes.

    +

    This is NOT for EdgeRouter devices.

    +
        Initialize attributes for establishing connection to target device.
    +
    +    :param ip: IP address of target device. Not required if `host` is
    +        provided.
    +    :type ip: str
    +
    +    :param host: Hostname of target device. Not required if `ip` is
    +            provided.
    +    :type host: str
    +
    +    :param username: Username to authenticate against target device if
    +            required.
    +    :type username: str
    +
    +    :param password: Password to authenticate against target device if
    +            required.
    +    :type password: str
    +
    +    :param secret: The enable password if target device requires one.
    +    :type secret: str
    +
    +    :param port: The destination port used to connect to the target
    +            device.
    +    :type port: int or None
    +
    +    :param device_type: Class selection based on device type.
    +    :type device_type: str
    +
    +    :param verbose: Enable additional messages to standard output.
    +    :type verbose: bool
    +
    +    :param global_delay_factor: Multiplication factor affecting Netmiko delays (default: 1).
    +    :type global_delay_factor: int
    +
    +    :param use_keys: Connect to target device using SSH keys.
    +    :type use_keys: bool
    +
    +    :param key_file: Filename path of the SSH key file to use.
    +    :type key_file: str
    +
    +    :param pkey: SSH key object to use.
    +    :type pkey: paramiko.PKey
    +
    +    :param passphrase: Passphrase to use for encrypted key; password will be used for key
    +            decryption if not specified.
    +    :type passphrase: str
    +
    +    :param allow_agent: Enable use of SSH key-agent.
    +    :type allow_agent: bool
    +
    +    :param ssh_strict: Automatically reject unknown SSH host keys (default: False, which
    +            means unknown SSH host keys will be accepted).
    +    :type ssh_strict: bool
    +
    +    :param system_host_keys: Load host keys from the users known_hosts file.
    +    :type system_host_keys: bool
    +    :param alt_host_keys: If `True` host keys will be loaded from the file specified in
    +            alt_key_file.
    +    :type alt_host_keys: bool
    +
    +    :param alt_key_file: SSH host key file to use (if alt_host_keys=True).
    +    :type alt_key_file: str
    +
    +    :param ssh_config_file: File name of OpenSSH configuration file.
    +    :type ssh_config_file: str
    +
    +    :param timeout: Connection timeout.
    +    :type timeout: float
    +
    +    :param session_timeout: Set a timeout for parallel requests.
    +    :type session_timeout: float
    +
    +    :param auth_timeout: Set a timeout (in seconds) to wait for an authentication response.
    +    :type auth_timeout: float
    +
    +    :param banner_timeout: Set a timeout to wait for the SSH banner (pass to Paramiko).
    +    :type banner_timeout: float
    +
    +    :param keepalive: Send SSH keepalive packets at a specific interval, in seconds.
    +            Currently defaults to 0, for backwards compatibility (it will not attempt
    +            to keep the connection alive).
    +    :type keepalive: int
    +
    +    :param default_enter: Character(s) to send to correspond to enter key (default:
    +
    +

    ). +:type default_enter: str

    +
        :param response_return: Character(s) to use in normalized return data to represent
    +            enter key (default:
    +
    +

    ) +:type response_return: str

    +
        :param fast_cli: Provide a way to optimize for performance. Converts select_delay_factor
    +            to select smallest of global and specific. Sets default global_delay_factor to .1
    +            (default: False)
    +    :type fast_cli: boolean
    +
    +    :param session_log: File path or BufferedIOBase subclass object to write the session log to.
    +    :type session_log: str
    +
    +    :param session_log_record_writes: The session log generally only records channel reads due
    +            to eliminate command duplication due to command echo. You can enable this if you
    +            want to record both channel reads and channel writes in the log (default: False).
    +    :type session_log_record_writes: boolean
    +
    +    :param session_log_file_mode: "write" or "append" for session_log file mode
    +            (default: "write")
    +    :type session_log_file_mode: str
    +
    +    :param allow_auto_change: Allow automatic configuration changes for terminal settings.
    +            (default: False)
    +    :type allow_auto_change: bool
    +
    +    :param encoding: Encoding to be used when writing bytes to the output channel.
    +            (default: ascii)
    +    :type encoding: str
    +
    +    :param sock: An open socket or socket-like object (such as a `.Channel`) to use for
    +            communication to the target host (default: None).
    +    :type sock: socket
    +
    +    :param global_cmd_verify: Control whether command echo verification is enabled or disabled
    +            (default: None). Global attribute takes precedence over function `cmd_verify`
    +            argument. Value of `None` indicates to use function `cmd_verify` argument.
    +    :type global_cmd_verify: bool|None
    +
    +
    +Source code +
    class UbiquitiUnifiSwitchSSH(UbiquitiEdgeSSH):
    +    def session_preparation(self):
    +        """
    +        Prepare the session after the connection has been established.
    +        When SSHing to a UniFi switch, the session initially starts at a Linux
    +        shell. Nothing interesting can be done in this environment, however,
    +        running `telnet localhost` drops the session to a more familiar
    +        environment.
    +        """
    +
    +        self._test_channel_read()
    +        self.set_base_prompt()
    +        self.send_command(
    +            command_string="telnet localhost", expect_string=r"\(UBNT\) >"
    +        )
    +        self.set_base_prompt()
    +        self.enable()
    +        self.disable_paging()
    +
    +        # Clear read buffer
    +        time.sleep(0.3 * self.global_delay_factor)
    +        self.clear_buffer()
    +
    +    def cleanup(self, command="exit"):
    +        """Gracefully exit the SSH session."""
    +        try:
    +            # The pattern="" forces use of send_command_timing
    +            if self.check_config_mode(pattern=""):
    +                self.exit_config_mode()
    +
    +            # Exit from the first 'telnet localhost'
    +            self.write_channel(command + self.RETURN)
    +        except Exception:
    +            pass
    +
    +        super().cleanup()
    +
    +

    Ancestors

    + +

    Methods

    +
    +
    +def session_preparation(self) +
    +
    +

    Prepare the session after the connection has been established. +When SSHing to a UniFi switch, the session initially starts at a Linux +shell. Nothing interesting can be done in this environment, however, +running telnet localhost drops the session to a more familiar +environment.

    +
    +Source code +
    def session_preparation(self):
    +    """
    +    Prepare the session after the connection has been established.
    +    When SSHing to a UniFi switch, the session initially starts at a Linux
    +    shell. Nothing interesting can be done in this environment, however,
    +    running `telnet localhost` drops the session to a more familiar
    +    environment.
    +    """
    +
    +    self._test_channel_read()
    +    self.set_base_prompt()
    +    self.send_command(
    +        command_string="telnet localhost", expect_string=r"\(UBNT\) >"
    +    )
    +    self.set_base_prompt()
    +    self.enable()
    +    self.disable_paging()
    +
    +    # Clear read buffer
    +    time.sleep(0.3 * self.global_delay_factor)
    +    self.clear_buffer()
    +
    +
    +
    +

    Inherited members

    + +
    +
    +
    +
    + +
    + + + + + \ No newline at end of file diff --git a/docs/netmiko/vyos/index.html b/docs/netmiko/vyos/index.html index d4a6393af..21a6cf9f6 100644 --- a/docs/netmiko/vyos/index.html +++ b/docs/netmiko/vyos/index.html @@ -185,6 +185,7 @@

    Classes

    self._test_channel_read() self.set_base_prompt() self.disable_paging(command="set terminal length 0") + self.set_terminal_width(command="set terminal width 512") # Clear the read buffer time.sleep(0.3 * self.global_delay_factor) self.clear_buffer() @@ -454,6 +455,7 @@

    Methods

    self._test_channel_read() self.set_base_prompt() self.disable_paging(command="set terminal length 0") + self.set_terminal_width(command="set terminal width 512") # Clear the read buffer time.sleep(0.3 * self.global_delay_factor) self.clear_buffer()
    diff --git a/docs/netmiko/vyos/vyos_ssh.html b/docs/netmiko/vyos/vyos_ssh.html index 56a6d45b9..b28129b2c 100644 --- a/docs/netmiko/vyos/vyos_ssh.html +++ b/docs/netmiko/vyos/vyos_ssh.html @@ -34,6 +34,7 @@

    Module netmiko.vyos.vyos_ssh

    self._test_channel_read() self.set_base_prompt() self.disable_paging(command="set terminal length 0") + self.set_terminal_width(command="set terminal width 512") # Clear the read buffer time.sleep(0.3 * self.global_delay_factor) self.clear_buffer() @@ -280,6 +281,7 @@

    Classes

    self._test_channel_read() self.set_base_prompt() self.disable_paging(command="set terminal length 0") + self.set_terminal_width(command="set terminal width 512") # Clear the read buffer time.sleep(0.3 * self.global_delay_factor) self.clear_buffer() @@ -549,6 +551,7 @@

    Methods

    self._test_channel_read() self.set_base_prompt() self.disable_paging(command="set terminal length 0") + self.set_terminal_width(command="set terminal width 512") # Clear the read buffer time.sleep(0.3 * self.global_delay_factor) self.clear_buffer() diff --git a/netmiko/__init__.py b/netmiko/__init__.py index 7180d1bdb..66b3d6cb1 100644 --- a/netmiko/__init__.py +++ b/netmiko/__init__.py @@ -23,7 +23,7 @@ # Alternate naming Netmiko = ConnectHandler -__version__ = "3.1.0" +__version__ = "3.1.1" __all__ = ( "ConnectHandler", "ssh_dispatcher", diff --git a/netmiko/aruba/aruba_ssh.py b/netmiko/aruba/aruba_ssh.py index 4cf9b3dcf..3338ccd0a 100644 --- a/netmiko/aruba/aruba_ssh.py +++ b/netmiko/aruba/aruba_ssh.py @@ -10,6 +10,9 @@ class ArubaSSH(CiscoSSHConnection): def __init__(self, **kwargs): if kwargs.get("default_enter") is None: kwargs["default_enter"] = "\r" + # Aruba has an auto-complete on space behavior that is problematic + if kwargs.get("global_cmd_verify") is None: + kwargs["global_cmd_verify"] = False return super().__init__(**kwargs) def session_preparation(self): @@ -33,3 +36,9 @@ def check_config_mode(self, check_string="(config) #", pattern=""): if not pattern: pattern = re.escape(self.base_prompt[:16]) return super().check_config_mode(check_string=check_string, pattern=pattern) + + def config_mode(self, config_command="configure term", pattern=""): + """ + Aruba auto completes on space so 'configure' needs fully spelled-out. + """ + return super().config_mode(config_command=config_command, pattern=pattern) diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py index fc295e594..185038cbf 100644 --- a/netmiko/base_connection.py +++ b/netmiko/base_connection.py @@ -870,18 +870,16 @@ def _sanitize_output( output = self.strip_prompt(output) return output - def establish_connection(self, width=None, height=None): + def establish_connection(self, width=511, height=1000): """Establish SSH connection to the network device Timeout will generate a NetmikoTimeoutException Authentication failure will generate a NetmikoAuthenticationException - width and height are needed for Fortinet paging setting. - - :param width: Specified width of the VT100 terminal window + :param width: Specified width of the VT100 terminal window (default: 511) :type width: int - :param height: Specified height of the VT100 terminal window + :param height: Specified height of the VT100 terminal window (default: 1000) :type height: int """ if self.protocol == "telnet": @@ -917,12 +915,9 @@ def establish_connection(self, width=None, height=None): print(f"SSH connection established to {self.host}:{self.port}") # Use invoke_shell to establish an 'interactive session' - if width and height: - self.remote_conn = self.remote_conn_pre.invoke_shell( - term="vt100", width=width, height=height - ) - else: - self.remote_conn = self.remote_conn_pre.invoke_shell() + self.remote_conn = self.remote_conn_pre.invoke_shell( + term="vt100", width=width, height=height + ) self.remote_conn.settimeout(self.blocking_timeout) if self.keepalive: @@ -1028,7 +1023,10 @@ def disable_paging(self, command="terminal length 0", delay_factor=1): log.debug(f"Command: {command}") self.write_channel(command) # Make sure you read until you detect the command echo (avoid getting out of sync) - output = self.read_until_pattern(pattern=re.escape(command.strip())) + if self.global_cmd_verify is not False: + output = self.read_until_pattern(pattern=re.escape(command.strip())) + else: + output = self.read_until_prompt() log.debug(f"{output}") log.debug("Exiting disable_paging") return output @@ -1051,7 +1049,10 @@ def set_terminal_width(self, command="", delay_factor=1): command = self.normalize_cmd(command) self.write_channel(command) # Make sure you read until you detect the command echo (avoid getting out of sync) - output = self.read_until_pattern(pattern=re.escape(command.strip())) + if self.global_cmd_verify is not False: + output = self.read_until_pattern(pattern=re.escape(command.strip())) + else: + output = self.read_until_prompt() return output def set_base_prompt( @@ -1618,7 +1619,10 @@ def config_mode(self, config_command="", pattern=""): if not self.check_config_mode(): self.write_channel(self.normalize_cmd(config_command)) # Make sure you read until you detect the command echo (avoid getting out of sync) - output += self.read_until_pattern(pattern=re.escape(config_command.strip())) + if self.global_cmd_verify is not False: + output += self.read_until_pattern( + pattern=re.escape(config_command.strip()) + ) if not re.search(pattern, output, flags=re.M): output += self.read_until_pattern(pattern=pattern) if not self.check_config_mode(): @@ -1638,7 +1642,10 @@ def exit_config_mode(self, exit_config="", pattern=""): if self.check_config_mode(): self.write_channel(self.normalize_cmd(exit_config)) # Make sure you read until you detect the command echo (avoid getting out of sync) - output += self.read_until_pattern(pattern=re.escape(exit_config.strip())) + if self.global_cmd_verify is not False: + output += self.read_until_pattern( + pattern=re.escape(exit_config.strip()) + ) if not re.search(pattern, output, flags=re.M): output += self.read_until_pattern(pattern=pattern) if self.check_config_mode(): diff --git a/netmiko/cisco/cisco_asa_ssh.py b/netmiko/cisco/cisco_asa_ssh.py index ac9f92bd2..5aece81ca 100644 --- a/netmiko/cisco/cisco_asa_ssh.py +++ b/netmiko/cisco/cisco_asa_ssh.py @@ -22,6 +22,9 @@ def session_preparation(self): except ValueError: # Don't fail for the terminal width pass + else: + # Disable cmd_verify if the terminal width can't be set + self.global_cmd_verify = False # Clear the read buffer time.sleep(0.3 * self.global_delay_factor) diff --git a/netmiko/cisco/cisco_nxos_ssh.py b/netmiko/cisco/cisco_nxos_ssh.py index 54da3d909..d1c67eff5 100644 --- a/netmiko/cisco/cisco_nxos_ssh.py +++ b/netmiko/cisco/cisco_nxos_ssh.py @@ -89,6 +89,8 @@ def remote_file_size(self, remote_cmd="", remote_file=None): remote_cmd = f"dir {self.file_system}/{remote_file}" remote_out = self.ssh_ctl_chan.send_command(remote_cmd) + if re.search("no such file or directory", remote_out, flags=re.I): + raise IOError("Unable to find file on remote system") # Match line containing file name escape_file_name = re.escape(remote_file) pattern = r".*({}).*".format(escape_file_name) @@ -96,12 +98,10 @@ def remote_file_size(self, remote_cmd="", remote_file=None): if match: file_size = match.group(0) file_size = file_size.split()[0] - - if "No such file or directory" in remote_out: - raise IOError("Unable to find file on remote system") - else: return int(file_size) + raise IOError("Unable to find file on remote system") + @staticmethod def process_md5(md5_output, pattern=r"= (.*)"): """Not needed on NX-OS.""" diff --git a/netmiko/huawei/__init__.py b/netmiko/huawei/__init__.py index 6b7d814d1..40b818c63 100644 --- a/netmiko/huawei/__init__.py +++ b/netmiko/huawei/__init__.py @@ -1,4 +1,5 @@ from netmiko.huawei.huawei import HuaweiSSH, HuaweiVrpv8SSH from netmiko.huawei.huawei import HuaweiTelnet +from netmiko.huawei.huawei_smartax import HuaweiSmartAXSSH -__all__ = ["HuaweiSSH", "HuaweiVrpv8SSH", "HuaweiTelnet"] +__all__ = ["HuaweiSmartAXSSH", "HuaweiSSH", "HuaweiVrpv8SSH", "HuaweiTelnet"] diff --git a/netmiko/huawei/huawei.py b/netmiko/huawei/huawei.py index c8d4ef201..adc8b1438 100644 --- a/netmiko/huawei/huawei.py +++ b/netmiko/huawei/huawei.py @@ -111,8 +111,9 @@ class HuaweiSSH(HuaweiBase): def special_login_handler(self): """Handle password change request by ignoring it""" - password_change_prompt = r"(Change now|Please choose 'YES' or 'NO').+" - output = self.read_until_prompt_or_pattern(password_change_prompt) + # Huawei can prompt for password change. Search for that or for normal prompt + password_change_prompt = r"((Change now|Please choose))|([\]>]\s*$)" + output = self.read_until_pattern(password_change_prompt) if re.search(password_change_prompt, output): self.write_channel("N\n") self.clear_buffer() diff --git a/netmiko/huawei/huawei_smartax.py b/netmiko/huawei/huawei_smartax.py new file mode 100644 index 000000000..6fd3e807e --- /dev/null +++ b/netmiko/huawei/huawei_smartax.py @@ -0,0 +1,82 @@ +import time +import re +from netmiko.cisco_base_connection import CiscoBaseConnection +from netmiko import log + + +class HuaweiSmartAXSSH(CiscoBaseConnection): + """Supports Huawei SmartAX and OLT.""" + + def session_preparation(self): + """Prepare the session after the connection has been established.""" + self.ansi_escape_codes = True + self._test_channel_read() + self.set_base_prompt() + self._disable_smart_interaction() + self.disable_paging() + # Clear the read buffer + time.sleep(0.3 * self.global_delay_factor) + self.clear_buffer() + + def strip_ansi_escape_codes(self, string_buffer): + """ + Huawei does a strange thing where they add a space and then add ESC[1D + to move the cursor to the left one. + The extra space is problematic. + """ + code_cursor_left = chr(27) + r"\[\d+D" + output = string_buffer + pattern = rf" {code_cursor_left}" + output = re.sub(pattern, "", output) + + log.debug("Stripping ANSI escape codes") + log.debug(f"new_output = {output}") + log.debug(f"repr = {repr(output)}") + return super().strip_ansi_escape_codes(output) + + def _disable_smart_interaction(self, command="undo smart", delay_factor=1): + """Disables the { } prompt to avoid having to sent a 2nd return after each command""" + delay_factor = self.select_delay_factor(delay_factor) + time.sleep(delay_factor * 0.1) + self.clear_buffer() + command = self.normalize_cmd(command) + log.debug("In disable_smart_interaction") + log.debug(f"Command: {command}") + self.write_channel(command) + if self.global_cmd_verify is not False: + output = self.read_until_pattern(pattern=re.escape(command.strip())) + else: + output = self.read_until_prompt() + log.debug(f"{output}") + log.debug("Exiting disable_smart_interaction") + + def disable_paging(self, command="scroll"): + return super().disable_paging(command=command) + + def config_mode(self, config_command="config", pattern=""): + """Enter configuration mode.""" + return super().config_mode(config_command=config_command, pattern=pattern) + + def check_config_mode(self, check_string=")#"): + return super().check_config_mode(check_string=check_string) + + def exit_config_mode(self, exit_config="return"): + return super().exit_config_mode(exit_config=exit_config) + + def check_enable_mode(self, check_string="#"): + return super().check_enable_mode(check_string=check_string) + + def enable(self, cmd="enable", pattern="", re_flags=re.IGNORECASE): + return super().enable(cmd=cmd, pattern=pattern, re_flags=re_flags) + + def set_base_prompt(self, pri_prompt_terminator=">", alt_prompt_terminator="#"): + return super().set_base_prompt( + pri_prompt_terminator=pri_prompt_terminator, + alt_prompt_terminator=alt_prompt_terminator, + ) + + def save_config(self, cmd="save", confirm=False, confirm_response=""): + """ Save Config for HuaweiSSH""" + return super().save_config( + cmd=cmd, confirm=confirm, confirm_response=confirm_response + ) diff --git a/netmiko/nokia/__init__.py b/netmiko/nokia/__init__.py old mode 100644 new mode 100755 index 87cabb5a3..c921cc047 --- a/netmiko/nokia/__init__.py +++ b/netmiko/nokia/__init__.py @@ -1,3 +1,3 @@ -from netmiko.nokia.nokia_sros_ssh import NokiaSrosSSH +from netmiko.nokia.nokia_sros_ssh import NokiaSrosSSH, NokiaSrosFileTransfer -__all__ = ["NokiaSrosSSH"] +__all__ = ["NokiaSrosSSH", "NokiaSrosFileTransfer"] diff --git a/netmiko/nokia/nokia_sros_ssh.py b/netmiko/nokia/nokia_sros_ssh.py old mode 100644 new mode 100755 index 35f49bf1f..4d71ef016 --- a/netmiko/nokia/nokia_sros_ssh.py +++ b/netmiko/nokia/nokia_sros_ssh.py @@ -7,10 +7,12 @@ # https://github.com/ktbyers/netmiko/blob/develop/LICENSE import re +import os import time from netmiko import log from netmiko.base_connection import BaseConnection +from netmiko.scp_handler import BaseFileTransfer class NokiaSrosSSH(BaseConnection): @@ -39,6 +41,8 @@ def session_preparation(self): # "@" indicates model-driven CLI (vs Classical CLI) if "@" in self.base_prompt: self.disable_paging(command="environment more false") + # To perform file operations we need to disable paging in classical-CLI also + self.disable_paging(command="//environment no more") self.set_terminal_width(command="environment console width 512") else: self.disable_paging(command="environment no more") @@ -89,7 +93,10 @@ def exit_config_mode(self, *args, **kwargs): output += self._discard() cmd = "quit-config" self.write_channel(self.normalize_cmd(cmd)) - output += self.read_until_pattern(pattern=re.escape(cmd)) + if self.global_cmd_verify is not False: + output += self.read_until_pattern(pattern=re.escape(cmd)) + else: + output += self.read_until_prompt() if self.check_config_mode(): raise ValueError("Failed to exit configuration mode") return output @@ -124,16 +131,25 @@ def commit(self, *args, **kwargs): log.info("Apply uncommitted changes!") cmd = "commit" self.write_channel(self.normalize_cmd(cmd)) - output += self.read_until_pattern(pattern=re.escape(cmd)) - output += self.read_until_pattern(r"@") + new_output = "" + if self.global_cmd_verify is not False: + new_output += self.read_until_pattern(pattern=re.escape(cmd)) + if "@" not in new_output: + new_output += self.read_until_pattern(r"@") + output += new_output return output def _exit_all(self): """Return to the 'root' context.""" + output = "" exit_cmd = "exit all" self.write_channel(self.normalize_cmd(exit_cmd)) # Make sure you read until you detect the command echo (avoid getting out of sync) - return self.read_until_pattern(pattern=re.escape(exit_cmd)) + if self.global_cmd_verify is not False: + output += self.read_until_pattern(pattern=re.escape(exit_cmd)) + else: + output += self.read_until_prompt() + return output def _discard(self): """Discard changes from private candidate for Nokia SR OS""" @@ -141,7 +157,9 @@ def _discard(self): if "@" in self.base_prompt: cmd = "discard" self.write_channel(self.normalize_cmd(cmd)) - new_output = self.read_until_pattern(pattern=re.escape(cmd)) + new_output = "" + if self.global_cmd_verify is not False: + new_output += self.read_until_pattern(pattern=re.escape(cmd)) if "@" not in new_output: new_output += self.read_until_prompt() output += new_output @@ -168,3 +186,102 @@ def cleanup(self, command="logout"): # Always try to send final 'logout'. self._session_log_fin = True self.write_channel(command + self.RETURN) + + +class NokiaSrosFileTransfer(BaseFileTransfer): + def __init__( + self, ssh_conn, source_file, dest_file, hash_supported=False, **kwargs + ): + super().__init__( + ssh_conn, source_file, dest_file, hash_supported=hash_supported, **kwargs + ) + + def _file_cmd_prefix(self): + """ + Allow MD-CLI to execute file operations by using classical CLI. + + Returns "//" if the current prompt is MD-CLI (empty string otherwise). + """ + return "//" if "@" in self.ssh_ctl_chan.base_prompt else "" + + def remote_space_available(self, search_pattern=r"(\d+)\s+\w+\s+free"): + """Return space available on remote device.""" + + # Sample text for search_pattern. + # " 3 Dir(s) 961531904 bytes free." + remote_cmd = self._file_cmd_prefix() + "file dir {}".format(self.file_system) + remote_output = self.ssh_ctl_chan.send_command(remote_cmd) + match = re.search(search_pattern, remote_output) + return int(match.group(1)) + + def check_file_exists(self, remote_cmd=""): + """Check if destination file exists (returns boolean).""" + + if self.direction == "put": + if not remote_cmd: + remote_cmd = self._file_cmd_prefix() + "file dir {}/{}".format( + self.file_system, self.dest_file + ) + dest_file_name = self.dest_file.replace("\\", "/").split("/")[-1] + remote_out = self.ssh_ctl_chan.send_command(remote_cmd) + if "File Not Found" in remote_out: + return False + elif dest_file_name in remote_out: + return True + else: + raise ValueError("Unexpected output from check_file_exists") + elif self.direction == "get": + return os.path.exists(self.dest_file) + + def remote_file_size(self, remote_cmd=None, remote_file=None): + """Get the file size of the remote file.""" + + if remote_file is None: + if self.direction == "put": + remote_file = self.dest_file + elif self.direction == "get": + remote_file = self.source_file + if not remote_cmd: + remote_cmd = self._file_cmd_prefix() + "file dir {}/{}".format( + self.file_system, remote_file + ) + remote_out = self.ssh_ctl_chan.send_command(remote_cmd) + + if "File Not Found" in remote_out: + raise IOError("Unable to find file on remote system") + + # Parse dir output for filename. Output format is: + # "10/16/2019 10:00p 6738 {filename}" + + pattern = r"\S+\s+\S+\s+(\d+)\s+{}".format(re.escape(remote_file)) + match = re.search(pattern, remote_out) + + if not match: + raise ValueError("Filename entry not found in dir output") + + file_size = int(match.group(1)) + return file_size + + def verify_file(self): + """Verify the file has been transferred correctly based on filesize.""" + if self.direction == "put": + return os.stat(self.source_file).st_size == self.remote_file_size( + remote_file=self.dest_file + ) + elif self.direction == "get": + return ( + self.remote_file_size(remote_file=self.source_file) + == os.stat(self.dest_file).st_size + ) + + def file_md5(self, **kwargs): + raise AttributeError("SR-OS does not support an MD5-hash operation.") + + def process_md5(self, **kwargs): + raise AttributeError("SR-OS does not support an MD5-hash operation.") + + def compare_md5(self, **kwargs): + raise AttributeError("SR-OS does not support an MD5-hash operation.") + + def remote_md5(self, **kwargs): + raise AttributeError("SR-OS does not support an MD5-hash operation.") diff --git a/netmiko/scp_functions.py b/netmiko/scp_functions.py index 452fcd781..b1f1357db 100644 --- a/netmiko/scp_functions.py +++ b/netmiko/scp_functions.py @@ -27,6 +27,7 @@ def file_transfer( inline_transfer=False, overwrite_file=False, socket_timeout=10.0, + verify_file=None, ): """Use Secure Copy or Inline (IOS-only) to transfer files to/from network devices. @@ -61,6 +62,10 @@ def file_transfer( if not cisco_ios and inline_transfer: raise ValueError("Inline Transfer only supported for Cisco IOS/Cisco IOS-XE") + # Replace disable_md5 argument with verify_file argument across time + if verify_file is None: + verify_file = not disable_md5 + scp_args = { "ssh_conn": ssh_conn, "source_file": source_file, @@ -76,13 +81,13 @@ def file_transfer( with TransferClass(**scp_args) as scp_transfer: if scp_transfer.check_file_exists(): if overwrite_file: - if not disable_md5: - if scp_transfer.compare_md5(): + if verify_file: + if scp_transfer.verify_file(): return nottransferred_but_verified else: # File exists, you can overwrite it, MD5 is wrong (transfer file) verifyspace_and_transferfile(scp_transfer) - if scp_transfer.compare_md5(): + if scp_transfer.verify_file(): return transferred_and_verified else: raise ValueError( @@ -94,16 +99,16 @@ def file_transfer( return transferred_and_notverified else: # File exists, but you can't overwrite it. - if not disable_md5: - if scp_transfer.compare_md5(): + if verify_file: + if scp_transfer.verify_file(): return nottransferred_but_verified msg = "File already exists and overwrite_file is disabled" raise ValueError(msg) else: verifyspace_and_transferfile(scp_transfer) # File doesn't exist - if not disable_md5: - if scp_transfer.compare_md5(): + if verify_file: + if scp_transfer.verify_file(): return transferred_and_verified else: raise ValueError("MD5 failure between source and destination files") diff --git a/netmiko/scp_handler.py b/netmiko/scp_handler.py index d4cebcd35..456473eb6 100644 --- a/netmiko/scp_handler.py +++ b/netmiko/scp_handler.py @@ -64,6 +64,7 @@ def __init__( file_system=None, direction="put", socket_timeout=10.0, + hash_supported=True, ): self.ssh_ctl_chan = ssh_conn self.source_file = source_file @@ -85,10 +86,12 @@ def __init__( self.file_system = file_system if direction == "put": - self.source_md5 = self.file_md5(source_file) + self.source_md5 = self.file_md5(source_file) if hash_supported else None self.file_size = os.stat(source_file).st_size elif direction == "get": - self.source_md5 = self.remote_md5(remote_file=source_file) + self.source_md5 = ( + self.remote_md5(remote_file=source_file) if hash_supported else None + ) self.file_size = self.remote_file_size(remote_file=source_file) else: raise ValueError("Invalid direction specified") diff --git a/netmiko/ssh_autodetect.py b/netmiko/ssh_autodetect.py index 9b05d1e88..c9e65f0d7 100644 --- a/netmiko/ssh_autodetect.py +++ b/netmiko/ssh_autodetect.py @@ -105,12 +105,27 @@ "priority": 99, "dispatch": "_autodetect_std", }, + "dell_os9": { + "cmd": "show system", + "search_patterns": [ + r"Dell Application Software Version: 9", + r"Dell Networking OS Version : 9", + ], + "priority": 99, + "dispatch": "_autodetect_std", + }, "dell_os10": { "cmd": "show version", "search_patterns": [r"Dell EMC Networking OS10-Enterprise"], "priority": 99, "dispatch": "_autodetect_std", }, + "dell_powerconnect": { + "cmd": "show system", + "search_patterns": [r"PowerConnect"], + "priority": 99, + "dispatch": "_autodetect_std", + }, "f5_tmsh": { "cmd": "show sys version", "search_patterns": [r"BIG-IP"], @@ -167,6 +182,17 @@ "priority": 99, "dispatch": "_autodetect_std", }, + "cisco_wlc": { + "dispatch": "_autodetect_remote_version", + "search_patterns": [r"CISCO_WLC"], + "priority": 99, + }, + "mellanox_mlnxos": { + "cmd": "show version", + "search_patterns": [r"Onyx", r"SX_PPC_M460EX"], + "priority": 99, + "dispatch": "_autodetect_std", + }, } @@ -288,7 +314,45 @@ def _send_command_wrapper(self, cmd): else: return cached_results - def _autodetect_std(self, cmd="", search_patterns=None, re_flags=re.I, priority=99): + def _autodetect_remote_version( + self, search_patterns=None, re_flags=re.IGNORECASE, priority=99 + ): + """ + Method to try auto-detect the device type, by matching a regular expression on the reported + remote version of the SSH server. + + Parameters + ---------- + search_patterns : list + A list of regular expression to look for in the reported remote SSH version + (default: None). + re_flags: re.flags, optional + Any flags from the python re module to modify the regular expression (default: re.I). + priority: int, optional + The confidence the match is right between 0 and 99 (default: 99). + """ + invalid_responses = [r"^$"] + + if not search_patterns: + return 0 + + try: + remote_version = self.connection.remote_conn.transport.remote_version + for pattern in invalid_responses: + match = re.search(pattern, remote_version, flags=re.I) + if match: + return 0 + for pattern in search_patterns: + match = re.search(pattern, remote_version, flags=re_flags) + if match: + return priority + except Exception: + return 0 + return 0 + + def _autodetect_std( + self, cmd="", search_patterns=None, re_flags=re.IGNORECASE, priority=99 + ): """ Standard method to try to auto-detect the device type. This method will be called for each device_type present in SSH_MAPPER_BASE dict ('dispatch' key). It will attempt to send a diff --git a/netmiko/ssh_dispatcher.py b/netmiko/ssh_dispatcher.py old mode 100644 new mode 100755 index 5d892ffc5..f45064914 --- a/netmiko/ssh_dispatcher.py +++ b/netmiko/ssh_dispatcher.py @@ -50,6 +50,7 @@ from netmiko.fortinet import FortinetSSH from netmiko.hp import HPProcurveSSH, HPProcurveTelnet, HPComwareSSH, HPComwareTelnet from netmiko.huawei import HuaweiSSH, HuaweiVrpv8SSH, HuaweiTelnet +from netmiko.huawei import HuaweiSmartAXSSH from netmiko.ipinfusion import IpInfusionOcNOSSSH, IpInfusionOcNOSTelnet from netmiko.juniper import JuniperSSH, JuniperTelnet, JuniperScreenOsSSH from netmiko.juniper import JuniperFileTransfer @@ -61,7 +62,7 @@ from netmiko.mrv import MrvLxSSH from netmiko.mrv import MrvOptiswitchSSH from netmiko.netapp import NetAppcDotSSH -from netmiko.nokia import NokiaSrosSSH +from netmiko.nokia import NokiaSrosSSH, NokiaSrosFileTransfer from netmiko.oneaccess import OneaccessOneOSTelnet, OneaccessOneOSSSH from netmiko.ovs import OvsLinuxSSH from netmiko.paloalto import PaloAltoPanosSSH @@ -77,6 +78,7 @@ from netmiko.terminal_server import TerminalServerSSH from netmiko.terminal_server import TerminalServerTelnet from netmiko.ubiquiti import UbiquitiEdgeSSH +from netmiko.ubiquiti import UbiquitiUnifiSwitchSSH from netmiko.vyos import VyOSSSH from netmiko.watchguard import WatchguardFirewareSSH @@ -140,6 +142,8 @@ "hp_comware": HPComwareSSH, "hp_procurve": HPProcurveSSH, "huawei": HuaweiSSH, + "huawei_smartax": HuaweiSmartAXSSH, + "huawei_olt": HuaweiSmartAXSSH, "huawei_vrpv8": HuaweiVrpv8SSH, "ipinfusion_ocnos": IpInfusionOcNOSSSH, "juniper": JuniperSSH, @@ -168,6 +172,7 @@ "sophos_sfos": SophosSfosSSH, "ubiquiti_edge": UbiquitiEdgeSSH, "ubiquiti_edgeswitch": UbiquitiEdgeSSH, + "ubiquiti_unifiswitch": UbiquitiUnifiSwitchSSH, "vyatta_vyos": VyOSSSH, "vyos": VyOSSSH, "watchguard_fireware": WatchguardFirewareSSH, @@ -184,6 +189,7 @@ "dell_os10": DellOS10FileTransfer, "juniper_junos": JuniperFileTransfer, "linux": LinuxFileTransfer, + "nokia_sros": NokiaSrosFileTransfer, } # Also support keys that end in _ssh @@ -220,6 +226,7 @@ CLASS_MAPPER["hp_procurve_telnet"] = HPProcurveTelnet CLASS_MAPPER["hp_comware_telnet"] = HPComwareTelnet CLASS_MAPPER["huawei_telnet"] = HuaweiTelnet +CLASS_MAPPER["huawei_olt_telnet"] = HuaweiSmartAXSSH CLASS_MAPPER["ipinfusion_ocnos_telnet"] = IpInfusionOcNOSTelnet CLASS_MAPPER["juniper_junos_telnet"] = JuniperTelnet CLASS_MAPPER["paloalto_panos_telnet"] = PaloAltoPanosTelnet diff --git a/netmiko/ubiquiti/__init__.py b/netmiko/ubiquiti/__init__.py index 066697941..214673edf 100644 --- a/netmiko/ubiquiti/__init__.py +++ b/netmiko/ubiquiti/__init__.py @@ -1,3 +1,4 @@ from netmiko.ubiquiti.edge_ssh import UbiquitiEdgeSSH +from netmiko.ubiquiti.unifiswitch_ssh import UbiquitiUnifiSwitchSSH -__all__ = ["UbiquitiEdgeSSH"] +__all__ = ["UbiquitiEdgeSSH", "UnifiSwitchSSH", "UbiquitiUnifiSwitchSSH"] diff --git a/netmiko/ubiquiti/unifiswitch_ssh.py b/netmiko/ubiquiti/unifiswitch_ssh.py new file mode 100644 index 000000000..a07021c7e --- /dev/null +++ b/netmiko/ubiquiti/unifiswitch_ssh.py @@ -0,0 +1,40 @@ +import time +from netmiko.ubiquiti.edge_ssh import UbiquitiEdgeSSH + + +class UbiquitiUnifiSwitchSSH(UbiquitiEdgeSSH): + def session_preparation(self): + """ + Prepare the session after the connection has been established. + When SSHing to a UniFi switch, the session initially starts at a Linux + shell. Nothing interesting can be done in this environment, however, + running `telnet localhost` drops the session to a more familiar + environment. + """ + + self._test_channel_read() + self.set_base_prompt() + self.send_command( + command_string="telnet localhost", expect_string=r"\(UBNT\) >" + ) + self.set_base_prompt() + self.enable() + self.disable_paging() + + # Clear read buffer + time.sleep(0.3 * self.global_delay_factor) + self.clear_buffer() + + def cleanup(self, command="exit"): + """Gracefully exit the SSH session.""" + try: + # The pattern="" forces use of send_command_timing + if self.check_config_mode(pattern=""): + self.exit_config_mode() + + # Exit from the first 'telnet localhost' + self.write_channel(command + self.RETURN) + except Exception: + pass + + super().cleanup() diff --git a/netmiko/vyos/vyos_ssh.py b/netmiko/vyos/vyos_ssh.py index 1efb031ea..c05a75c51 100644 --- a/netmiko/vyos/vyos_ssh.py +++ b/netmiko/vyos/vyos_ssh.py @@ -10,6 +10,7 @@ def session_preparation(self): self._test_channel_read() self.set_base_prompt() self.disable_paging(command="set terminal length 0") + self.set_terminal_width(command="set terminal width 512") # Clear the read buffer time.sleep(0.3 * self.global_delay_factor) self.clear_buffer() diff --git a/tests/conftest.py b/tests/conftest.py index 1150a9455..14c010d34 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -223,6 +223,20 @@ def delete_file_ciena_saos(ssh_conn, dest_file_system, dest_file): return output +def delete_file_nokia_sros(ssh_conn, dest_file_system, dest_file): + """Delete a remote file for a Nokia SR OS device.""" + full_file_name = "{}/{}".format(dest_file_system, dest_file) + cmd = "file delete {} force".format(full_file_name) + cmd_prefix = "" + if "@" in ssh_conn.base_prompt: + cmd_prefix = "//" + ssh_conn.send_command(cmd_prefix + "environment no more") + output = ssh_conn.send_command_timing( + cmd_prefix + cmd, strip_command=False, strip_prompt=False + ) + return output + + @pytest.fixture(scope="module") def scp_fixture(request): """ @@ -474,4 +488,9 @@ def get_platform_args(): "enable_scp": False, "delete_file": delete_file_ciena_saos, }, + "nokia_sros": { + "file_system": "cf3:", + "enable_scp": False, + "delete_file": delete_file_nokia_sros, + }, } diff --git a/tests/etc/commands.yml.example b/tests/etc/commands.yml.example index 4e69c619c..bb254cab1 100644 --- a/tests/etc/commands.yml.example +++ b/tests/etc/commands.yml.example @@ -3,20 +3,61 @@ cisco_ios: version: "show version" basic: "show ip interface brief" + wide_command: "show ip access-lists myverybiglongaccesslistthatdoesntexistandwherethisexceeds80characterssolinewrappingoccurs" extended_output: "show version" # requires paging to be disabled - config: + config: - "logging buffered 20000" # base command - "no logging console" - "logging buffered 20010" # something you can verify has changed config_verification: "show run | inc logging buffer" - config_file: 'cisco_ios_commands.txt' - save_config_cmd: 'copy run start' + config_file: "cisco_ios_commands.txt" + save_config_confirm: False + +cisco_ios_telnet: + version: "show version" + basic: "show ip interface brief" + wide_command: "show ip access-lists myverybiglongaccesslistthatdoesntexistandwherethisexceeds80characterssolinewrappingoccurs" + extended_output: "show version" # requires paging to be disabled + config: + - "logging buffered 20000" # base command + - "no logging console" + - "logging buffered 20010" # something you can verify has changed + config_verification: "show run | inc logging buffer" + config_file: "cisco_ios_commands.txt" + save_config_confirm: False + +cisco_xr: + version: "show version" + basic: "show ip interface brief" + basic_textfsm: "show interface brief" + wide_command: "show access-list myverybiglongaccesslistthatdoesntexistandwherethisexceeds80characterssolinewrappingoccurs" + extended_output: "show version" # requires paging to be disabled + config: + - "logging buffered 4000000" # base command + - "no logging console" + - "logging buffered 4000010" # something you can verify has changed + config_verification: "show run | inc logging buffer" + support_commit: True + commit_verification: "show configuration commit list 1 detail" + +cisco_s300: + version: "show version" + basic: "show ip interface" + wide_command: "show ip access-lists myverybiglongaccesslistthatdoesntexistandwherethisexceeds80characterssolinewrappingoccurs" + extended_output: "show run" # requires paging to be disabled + config: + - 'logging buffered notifications' + - 'no logging console' + - 'logging buffered warnings' + config_verification: "show run" + config_file: "cisco_ios_commands.txt" save_config_confirm: True - save_config_response: '' cisco_asa: version: "show version" basic: "show ip" + wide_command: "show access-list myverybiglongaccesslistthatdoesntexistandwherethisexceeds80characterssolinewrappingoccurs" + basic_textfsm: "show route" extended_output: "show version" # requires paging to be disabled config: - 'logging buffered notifications' @@ -28,8 +69,9 @@ cisco_asa: arista_eos: version: "show version" basic: "show ip int brief" + wide_command: "show ip access-lists myverybiglongaccesslistthatdoesntexistandwherethisexceeds80characterssolinewrappingoccurs" extended_output: "show logging" # requires paging to be disabled - config: + config: - "logging buffered 20000" - "no logging console" - "logging buffered 20010" @@ -38,18 +80,30 @@ arista_eos: hp_procurve: version: "show version" basic: "show ip" + basic_textfsm: "show system" extended_output: "show logging" # requires paging to be disabled - config: + config: - 'time timezone -420' - 'time daylight-time-rule Continental-US-and-Canada' - 'time timezone -480' - config_verification: "show run" - -juniper: + config_verification: "show run" + +hp_comware: + version: "display version" + basic: "display ip int brief" + extended_output: "display version" # requires paging to be disabled + config: + - 'ip host test1 1.1.1.1' + - 'undo ip host test1 1.1.1.1' + - 'ip host test1 1.1.1.2' + config_verification: "display current-configuration" + +juniper_junos: version: "show version" basic: "show interfaces terse" + basic_textfsm: "show interfaces" extended_output: "show configuration" # requires paging to be disabled - config: + config: - 'set system syslog archive size 110k files 3' - 'set system time-zone America/New_York' - 'set system syslog archive size 120k files 3' @@ -58,6 +112,46 @@ juniper: rollback: 'rollback 0' commit_verification: "run show system commit" +linux: + version: "uname -a" + basic: "ifconfig -a | grep inet | grep -v inet6 " + wide_command: 'echo "cable modem deny 0015.f2fe.ba11"; echo "cable modem deny 0015.f2fe.ba12"; echo "cable modem deny 0015.f2fe.ba13"' + extended_output: "netstat -an" # requires paging to be disabled + # config_long_command: "ls verylongcommandnamethatwillcauselinewrapissuesasitdoesnotfitonesinglescreengreaterthan127charsandthensomesothingsreallyarennotright" + # config_verification: "ls" + +dell_force10: + version: "show version" + basic: "show ip interface brief managementethernet 0/0" + extended_output: "show ip interface brief" # requires paging to be disabled + config: + - "logging buffered 50000" # base command + - "no logging console" + - "logging buffered 50010" # something you can verify has changed + config_verification: "show run" + +cisco_nxos: + version: "show version" + basic: "show ip interface brief vrf management" + basic_textfsm: "show interface brief" + extended_output: "show logging" # requires paging to be disabled + config: + - "logging console 0" # base command + - "no logging console" + - "logging console 4" # something you can verify has changed + config_verification: "show run | inc logging" + config_long_command: "snmp-server location verylongsnmplocationnamethatwillcauselinewrapissuesasitdoesnotfitonesinglescreengreaterthan127charsandthensomesothingsreallyarennotright" + +cisco_xe: + version: "show version" + basic: "show ip interface brief" + extended_output: "show version" # requires paging to be disabled + config: + - "logging buffered 20000" # base command + - "no logging console" + - "logging buffered 20010" # something you can verify has changed + config_verification: "show run | inc logging buffer" + juniper_screenos: version: "get system version" basic: "get route" @@ -137,6 +231,16 @@ ubiquiti_edge: - "logging persistent 4" config_verification: "show running-config | include 'logging'" +ubiquiti_unifiswitch: + version: "show version" + basic: "show network" + extended_output: "show running-config" + config: + - "logging persistent 3" + - "no logging persistent" + - "logging persistent 4" + config_verification: "show running-config | include 'logging'" + dellos10: version: "show version" basic: "show ip interface brief" @@ -316,3 +420,10 @@ sophos_sfos: basic: "system diagnostics utilities route lookup 172.16.16.16" extended_output: "system diagnostics show version-info" +huawei_smartax: + version: "display version" + basic: "display system sys-info" + extended_output: "display version" + config: + - acl 2456 + config_verification: "display current-configuration | include acl 2456" diff --git a/tests/etc/responses.yml.example b/tests/etc/responses.yml.example index 6b4f9f6e2..886b28e91 100644 --- a/tests/etc/responses.yml.example +++ b/tests/etc/responses.yml.example @@ -89,6 +89,16 @@ ubiquiti_edge: cmd_response_init: "" cmd_response_final: "logging persistent 4" +ubiquiti_unifiswitch: + base_prompt: "(UBNT) " + router_prompt: "(UBNT) >" + enable_prompt: "(UBNT) #" + interface_ip: 10.0.132.4 + version_banner: "Software Version" + multiple_line_output: "Current Configuration:" + cmd_response_init: "" + cmd_response_final: "logging persistent 4" + dellos10: base_prompt: OS10 router_prompt : OS10# @@ -258,3 +268,10 @@ sophos_sfos: version_banner: "Serial Number" multiple_line_output: "" +huawei_smartax: + base_prompt: "ol01.test-lab.xyz" + router_prompt: "ol01.test-lab.xyz>" + enable_prompt: "ol01.test-lab.xyz#" + interface_ip: 192.0.2.1 + version_banner: "VERSION :" + multiple_line_output: "" diff --git a/tests/etc/test_devices.yml.example b/tests/etc/test_devices.yml.example index 91e48b968..12c287fcb 100644 --- a/tests/etc/test_devices.yml.example +++ b/tests/etc/test_devices.yml.example @@ -203,4 +203,8 @@ sophos_sfos: username: admin password: admin - +huawei_smartax: + device_type: huawei_smartax + ip: 192.0.2.1 + username: TEST + password: TEST diff --git a/tests/test_huawei_smartax.sh b/tests/test_huawei_smartax.sh new file mode 100644 index 000000000..9a0910615 --- /dev/null +++ b/tests/test_huawei_smartax.sh @@ -0,0 +1,7 @@ +echo "Huawei SmartAX SSH" \ +&& date \ +&& py.test -v test_netmiko_show.py --test_device huawei_smartax \ +&& date \ +|| RETURN_CODE=1 + +exit $RETURN_CODE diff --git a/tests/test_netmiko_scp.py b/tests/test_netmiko_scp.py index 9852a1037..fb90d6435 100755 --- a/tests/test_netmiko_scp.py +++ b/tests/test_netmiko_scp.py @@ -2,17 +2,6 @@ import pytest from netmiko import file_transfer -# def test_enable_scp(scp_fixture): -# ssh_conn, scp_transfer = scp_fixture -# -# scp_transfer.disable_scp() -# output = ssh_conn.send_command_expect("show run | inc scp") -# assert 'ip scp server enable' not in output -# -# scp_transfer.enable_scp() -# output = ssh_conn.send_command_expect("show run | inc scp") -# assert 'ip scp server enable' in output - def test_scp_put(scp_fixture): ssh_conn, scp_transfer = scp_fixture @@ -53,6 +42,8 @@ def test_remote_file_size(scp_fixture): def test_md5_methods(scp_fixture): ssh_conn, scp_transfer = scp_fixture + if "nokia_sros" in ssh_conn.device_type: + pytest.skip("MD5 not supported on this platform") md5_value = "d8df36973ff832b564ad84642d07a261" remote_md5 = scp_transfer.remote_md5() @@ -70,7 +61,7 @@ def test_verify_space_available_get(scp_fixture_get): ssh_conn, scp_transfer = scp_fixture_get assert scp_transfer.verify_space_available() is True # intentional make there not be enough space available - scp_transfer.file_size = 100000000000000 + scp_transfer.file_size = 100000000000000000 assert scp_transfer.verify_space_available() is False @@ -90,6 +81,8 @@ def test_scp_get(scp_fixture_get): def test_md5_methods_get(scp_fixture_get): ssh_conn, scp_transfer = scp_fixture_get + if "nokia_sros" in ssh_conn.device_type: + pytest.skip("MD5 not supported on this platform") md5_value = "d8df36973ff832b564ad84642d07a261" local_md5 = scp_transfer.file_md5("test9.txt") assert local_md5 == md5_value diff --git a/tests/test_netmiko_show.py b/tests/test_netmiko_show.py index 44b3e3dfd..24309a08e 100755 --- a/tests/test_netmiko_show.py +++ b/tests/test_netmiko_show.py @@ -35,6 +35,14 @@ def test_disable_paging(net_connect, commands, expected_responses): assert expected_responses["multiple_line_output"] in multiple_line_output +def test_terminal_width(net_connect, commands, expected_responses): + """Verify long commands work properly.""" + wide_command = commands.get("wide_command") + if wide_command: + net_connect.send_command(wide_command) + assert True + + def test_ssh_connect(net_connect, commands, expected_responses): """Verify the connection was established successfully.""" show_version = net_connect.send_command(commands["version"]) diff --git a/tests/test_suite_alt.sh b/tests/test_suite_alt.sh index ba7e03f82..865a549d5 100755 --- a/tests/test_suite_alt.sh +++ b/tests/test_suite_alt.sh @@ -71,6 +71,11 @@ echo "Starting tests...good luck:" \ && py.test -v test_netmiko_config.py --test_device cisco_xrv \ && py.test -v test_netmiko_commit.py --test_device cisco_xrv \ \ +&& echo "Cisco IOS-XR (Azure)" \ +&& py.test -v test_netmiko_show.py --test_device cisco_xr_azure \ +&& py.test -v test_netmiko_config.py --test_device cisco_xr_azure \ +&& py.test -v test_netmiko_commit.py --test_device cisco_xr_azure \ +\ && echo "Cisco NXOS" \ && py.test -v test_netmiko_scp.py --test_device nxos1 \ && py.test -v test_netmiko_show.py --test_device nxos1 \ @@ -86,6 +91,7 @@ echo "Starting tests...good luck:" \ && py.test -s -v test_netmiko_autodetect.py --test_device juniper_srx \ && py.test -s -v test_netmiko_autodetect.py --test_device cisco_asa \ && py.test -s -v test_netmiko_autodetect.py --test_device cisco_xrv \ +&& py.test -s -v test_netmiko_autodetect.py --test_device cisco_xr_azure \ \ && echo "HP ProCurve" \ && py.test -v test_netmiko_show.py --test_device hp_procurve \ diff --git a/tests/test_ubiquiti_unifi.sh b/tests/test_ubiquiti_unifi.sh new file mode 100755 index 000000000..0d7fc0b48 --- /dev/null +++ b/tests/test_ubiquiti_unifi.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +RETURN_CODE=0 + +# Exit on the first test failure and set RETURN_CODE = 1 +echo "Starting tests...good luck:" \ +&& echo "Ubiquiti UniFi Switch" \ +&& py.test -v test_netmiko_show.py --test_device ubiquiti_unifi \ +&& py.test -v test_netmiko_config.py --test_device ubiquiti_unifi \ +|| RETURN_CODE=1 + +exit $RETURN_CODE