Repository: ktbyers/netmiko

Issue:
Output split across two reads so send_command pattern never detected
Hi folks, 
I have a strange problem with the build-in `send_command()` method and I am not able to find a good solution to it.
I have a script which connects to several Cisco IOS-XR routers and issue some commands. In most cases it is working just fine, but sometimes there is this error message: `Search pattern never detected in send_command_expect`.

Here is my simplified code:
```
router_params = {
                    "host": router_hostname,
                    "username": username,
                    "password": password,
                    "device_type": 'cisco_xr',
                    "ssh_config_file": ssh_config_file
                }
                
router = ConnectHandler(**router_params)
router.send_command('show int description | e down | i R2-R2,', delay_factor=2)
router.send_command('show ipv4 int brief | e down | e Loop | e unassigned | i default', delay_factor=2)
```

When I tried to debug it I noticed strange behavior in the `self.read_channel()` method so I put some additional print() statements into the `send_command()` method as follow:
```
....
# Keep reading data until search_pattern is found or until max_loops is reached.
while i <= max_loops:
    print(f'Searching for prompt: {search_pattern}')          <---- MY DEBUG LINE
    new_data = self.read_channel()
    print(f'new_data: {new_data}')                            <---- MY DEBUG LINE
.....
```

And here is the output when the `Search pattern never detected in send_command_expect` is raised:

```
Interactive SSH session established
Searching for prompt: RP/0/RP0/CPU0:fra\-xxxx\-yyyy\#
new_data: 
Searching for prompt: RP/0/RP0/CPU0:fra\-xxxx\-yyyy\#
new_data: show int description | e down | i R2-R2,
Thu Jan  3 19:15:49.283 CET

Searching for prompt: RP/0/RP0/CPU0:fra\-xxxx\-yyyy\#
new_data: 
Searching for prompt: RP/0/RP0/CPU0:fra\-xxxx\-yyyy\#
new_data: Te0/0/0/0          up          up          R2-R2,abcde
RP/0/RP0/CPU0:fra-xxxx-yyyy#
Searching for prompt: RP/0/RP0/CPU0:fra\-xxxx\-yyyy\#
new_data: 
Searching for prompt: RP/0/RP0/CPU0:fra\-xxxx\-yyyy\#
new_data: 
Searching for prompt: RP/0/RP0/CPU0:fra\-xxxx\-yyyy\#
new_data: show ipv4 int brief | e down | e Loop | e unassigned | i default
Thu Jan  3 19:15:50.564 CET
TenGigE0/0/0/0                 10.1.1.1    Up              Up       default 
TenGigE0/0/0/18                10.1.2.1   Up              Up       default 
TenGigE0/1/0/18                10.1.3.1   Up              Up       default 
RP/0/RP0/C
Searching for prompt: RP/0/RP0/CPU0:fra\-xxxx\-yyyy\#
new_data: PU0:fra-xxxx-yyyy#
Searching for prompt: RP/0/RP0/CPU0:fra\-xxxx\-yyyy\#
new_data: 
Searching for prompt: RP/0/RP0/CPU0:fra\-xxxx\-yyyy\#
new_data: 
.......
Search pattern never detected in send_command_expect: RP/0/RP0/CPU0:fra\-xxxx\-yyyy\#
```

If you take a closer look especially into this line:
```
RP/0/RP0/C
Searching for prompt: RP/0/RP0/CPU0:fra\-xxxx\-yyyy\#
new_data: PU0:fra-xxxx-yyyy#
```
you will see the router prompt `RP/0/RP0/CPU0:fra-xxxx-yyyy#` is split to 2 outputs from the `self.read_channel()` and that is the reason why the prompt is never found and the error `Search pattern never detected in send_command_expect` is issued.

I suspect some delay problem with this but I also think the behavior of the `self.read_channel()` method could be improved as it reads in the middle of the output which could cause problems like this. 



Discussion:
@dorko20 Yes, that case is possible. 

I am worried that case is going to be hard to fix--or worded differently I suspect that there are possibly some negative trade-offs in fixing that (mainly slowing things down meaningfully and evaluating the entire output string and not just the new section of the string). Note, the output string could potentially be enormous, for example, 'show ip bgp' of the entire routing table.

I guess we could do a compromises and keep like three or some small numbers of contiguous reads. The issue would still be possible, but much mess likely to happen.

Resolving patch:
diff --git a/netmiko/base_connection.py b/netmiko/base_connection.py
--- a/netmiko/base_connection.py
+++ b/netmiko/base_connection.py
@@ -15,6 +15,7 @@
 import socket
 import telnetlib
 import time
+from collections import deque
 from os import path
 from threading import Lock
 
@@ -1256,6 +1257,7 @@ def send_command(
 
         i = 1
         output = ""
+        past_three_reads = deque(maxlen=3)
         first_line_processed = False
 
         # Keep reading data until search_pattern is found or until max_loops is reached.
@@ -1265,10 +1267,12 @@ def send_command(
                 if self.ansi_escape_codes:
                     new_data = self.strip_ansi_escape_codes(new_data)
 
+                output += new_data
+                past_three_reads.append(new_data)
+
                 # Case where we haven't processed the first_line yet (there is a potential issue
                 # in the first line (in cases where the line is repainted).
                 if not first_line_processed:
-                    output += new_data
                     output, first_line_processed = self._first_line_handler(
                         output, search_pattern
                     )
@@ -1277,9 +1281,8 @@ def send_command(
                         break
 
                 else:
-                    output += new_data
-                    # Check if pattern is in the incremental data
-                    if re.search(search_pattern, new_data):
+                    # Check if pattern is in the past three reads
+                    if re.search(search_pattern, "".join(past_three_reads)):
                         break
 
             time.sleep(delay_factor * loop_delay)


Test patch: