Netmiko4 read_timeout

Author: Kirk Byers
Date: March 2, 2022
Netmiko Logo

Though this be madness, yet there is method in't.

Netmiko has a significant problem which has annoyed and frustrated me for a long time.

This problem is delay factor and global delay factor.

One of the fundamental difficulties of screen-scraping is that there is no good notion of a command being done. Consequently, any screen-scraping library has to have solutions to overcome this.

Delay factor and global delay factor was one of those solutions (though in retrospect probably not a very good one).

Without going into a lot of detail, delay factor and global delay factor are both means in Netmiko of slowing things down (basically multiplying delays in certain contexts).

Now slowing things down is fine, but for Netmiko users it is not readily apparent what values to assign these arguments. Nor is it readily apparent how they interact with fast_cli (another Netmiko argument).

What Netmiko users need is a simple timeout. In other words, wait x-amount of time and if the trailing prompt is not returned, then give up and raise an exception.

This is what Netmiko 4.x does.

The Netmiko4 Solution (read_timeout)

First a bit of context 'send_command()' is Netmiko's main method for retrieving show information from devices.

By default, 'send_command()' will try to find the trailing router prompt in the output. You can also explicitly specify a regular expression pattern to use as the terminating string. This is done by using the 'expect_string' argument.

For example:

# Execute cmd and look for the trailing prompt
net_connect.send_command("show ip int brief")

# Execute cmd and look for terminating pattern
net_connect.send_command(
    "show ip int brief", 
    expect_string=r"#"
) 

In Netmiko 3.4.0 'send_command()' will search through the device's output looking for the router prompt or pattern for ten seconds. This assumes fast_cli=True and the default delay factor and global delay factor. If the prompt or pattern is not detected in ten seconds, then an exception will be raised.

Netmiko4 behaves similarly except it has a new read_timeout argument. This argument also defaults to ten seconds.

# Execute cmd and look for the trailing prompt
net_connect.send_command(
    "show ip int brief", 
    read_timeout=10
)

# Execute cmd and look for terminating pattern
net_connect.send_command(
    "show ip int brief", 
    expect_string=r"#", 
    read_timeout=10
) 

Here I made the default read_timeout explicit.

A More Complex Example

What does this look like in a more complex example?

import os
from netmiko import ConnectHandler
from datetime import datetime

password = os.environ["NETMIKO_PASSWORD"]

device = {
    "device_type": "cisco_ios",
    "host": "cisco3.lasthop.io",
    "username": "pyclass",
    "password": password,
}

with ConnectHandler(**device) as net_connect:
    print(net_connect.find_prompt())
    try:
        start_time = datetime.now()
        output = net_connect.send_command("show tech-support")
    finally:
        end_time = datetime.now()
        print(f"\nExec time: {end_time - start_time}\n")

    # Print just the very beginning of the output
    print("\n".join(output.splitlines()[:3])) 

Here you can see that I am executing 'show tech-support'.

On this particular device, 'show tech-support' takes about 75 seconds to execute. Additionally, you can see in the 'send_command()' method call that I have not explicitly specified a read_timeout. Consequently, Netmiko is using the default read_time of ten seconds.

Given the above, we expect this command will fail with an exception after waiting ten seconds.

Note, the code above contains a few special items so that I can track the execution time of send_command() and display this time to the screen.

When we execute this program we see the following:

$ python new_read_timeout.py 
cisco3#

Exec time: 0:00:10.042891

Traceback (most recent call last):
  File "/home/kbyers/pynet/netmiko4/read_timeout/new_read_timeout.py", line 18, in 
    output = net_connect.send_command("show tech-support")
  File "/home/kbyers/netmiko/netmiko/utilities.py", line 600, in wrapper_decorator
    return func(self, *args, **kwargs)
  File "/home/kbyers/netmiko/netmiko/base_connection.py", line 1660, in send_command
    raise ReadTimeout(msg)
netmiko.exceptions.ReadTimeout: 
Pattern not detected: 'cisco3\\#' in output.

Things you might try to fix this:
1. Explicitly set your pattern using the expect_string argument.
2. Increase the read_timeout to a larger value.

You can also look at the Netmiko session_log or debug log for more information.

As expected 'send_command()' fails in ten seconds and it raises an exception (in this case a ReadTimeout exception).

Additionally, Netmiko provides us with some hints on how we might fix the issue (namely increase the 'read_timeout' to a larger value).

    try:
        start_time = datetime.now()
        output = net_connect.send_command(
            "show tech-support", 
            read_timeout=90
        )
    finally:
        end_time = datetime.now()
        print(f"\nExec time: {end_time - start_time}\n") 

Here you can see that after setting the read_timeout to ninety seconds that the execution worked properly:

$ python new_read_timeout.py 
cisco3#

Exec time: 0:01:17.150191


------------------ show clock ------------------

Drilling into the above behavior a bit more—Netmiko used the 'send_command()' method to execute 'show tech-support'. As part of this method, Netmiko entered a loop searching for either the trailing router prompt or waiting until its read_timeout timer had expired.

After roughly 77 seconds, Netmiko detected the trailing router prompt.

Once it encountered this trailing prompt, Netmiko then returned the command output.

The Guessing Game—How Long to Wait?

Now there is a bit of a guessing game going on here.

You need to know beforehand how long Netmiko should wait before giving up.

Thankfully, you can just set the read_timeout to a relatively large value and Netmiko will complete once it encounters the trailing prompt.

In other words, if you set the read_timeout to 300 seconds and 77 seconds later, Netmiko encounters the "cisco3#" prompt, then Netmiko will complete the 'send_command()' method and return the output (at 77 seconds).

The main downside of this large read_timeout value is the extra waiting time when your 'send_command()' call fails.

In other words, when something goes wrong, you will have to wait for read_timeout seconds before Netmiko raises an exception. So if you set read_timeout to 600 seconds, then Netmiko won't raise a failure exception until those 10-minutes have passed.

Because of this, I have set the default read_timeout value to ten seconds. This allocates sufficient time that the vast majority of standard commands should complete, but also avoids long delays when things go wrong.

Backwards Compatibility

Since this new 'read_timeout' is a major change to Netmiko's behavior, I have implemented a compatibility argument that will cause Netmiko to revert back to a similar behavior as Netmiko 3.4.0.

This argument is named 'delay_factor_compat' and needs to be set to True to revert to a behavior similar to Netmiko 3.4.0.

For example:

import os
from netmiko import ConnectHandler
from datetime import datetime

password = os.environ["NETMIKO_PASSWORD"]

device = {
    "device_type": "cisco_ios",
    "host": "cisco3.lasthop.io",
    "username": "pyclass",
    "password": password,
    "delay_factor_compat": True,
    "fast_cli": False,
}

with ConnectHandler(**device) as net_connect:
    print(net_connect.find_prompt())
    try:
        start_time = datetime.now()
        output = net_connect.send_command(
            "show tech-support", 
            delay_factor=2
        )
    finally:
        end_time = datetime.now()
        print(f"Exec time: {end_time - start_time}")

    # Print just the very beginning of the output
    print("\n".join(output.splitlines()[:3])) 

Executing this yields:

$ python back_compat.py 
cisco3#
/home/kbyers/netmiko/netmiko/base_connection.py:1591: DeprecationWarning: 

You have chosen to use Netmiko's delay_factor compatibility mode for
send_command. This will revert Netmiko to behave similarly to how it
did in Netmiko 3.x (i.e. to use delay_factor/global_delay_factor and
max_loops).

Using these parameters Netmiko has calculated an effective read_timeout
of 200.0 and will set the read_timeout to this value.

Please convert your code to that new format i.e.:

    net_connect.send_command(cmd, read_timeout=200.0)

And then disable delay_factor_compat.

delay_factor_compat will be removed in Netmiko 5.x.

  warnings.warn(msg, DeprecationWarning)
Exec time: 0:01:16.804687

------------------ show clock ------------------

Above I had explicitly enabled Netmiko deprecation warnings so they would show up on standard output.

$ env | grep PYTHONWARNINGS
PYTHONWARNINGS=always 

Netmiko 3.4.0 with fast_cli=False and delay_factor=2 should allocate 200 seconds for send_command() to complete.

This is consistent with the calculation shown in the Netmiko 4.x warning message and also consistent with 'show tech-support' completing successfully in 77 seconds.

Overriding the Read Timeout

In certain situations you might need to override the default read_timeout.

For example, if you are using Netmiko inside a different library or framework (NAPALM, Nornir, Salt), then adjusting the individual read_timeout argument used in send_command() might be impractical.

In such cases, the read_timeout override likely needs to happen at the object-level (i.e. when the object is created).

This can be accomplished using the read_timeout_override argument. This is a Netmiko ConnectHandler argument.

import os
from netmiko import ConnectHandler
from datetime import datetime

password = os.environ["NETMIKO_PASSWORD"]

device = {
    "device_type": "cisco_ios",
    "host": "cisco3.lasthop.io",
    "username": "pyclass",
    "password": password,
    "read_timeout_override": 90,
}

with ConnectHandler(**device) as net_connect:
    print(net_connect.find_prompt())
    try:
        start_time = datetime.now()
        output = net_connect.send_command("show tech-support")
    finally:
        end_time = datetime.now()
        print(f"Exec time: {end_time - start_time}")

    # Print just the very beginning of the output
    print("\n".join(output.splitlines()[:3]))  

Here we have used the "read_timeout_override" argument to set the send_command read_timeout to always be ninety seconds.

This argument will override the default read_timeout. It will also override any specific read_timeout passed into send_command method.

For example, if a library such as NAPALM specifies send_command(command, read_timeout=25), then the read_timeout_override would take precedence over this value.

Kirk Byers

@kirkbyers

You might also be interested in: