pythonmultithreadingtelnetlib

Python Threading and Telnet unexpected results


I wrote a script that telnets to some network equipment and pulls the inventory and places it in a file. I used threading to speed up the script as I have around 160 nodes. I wrote a function that telnets to each node, retrieves the inventory, writes the output to a file, then disconnects the session. The script worked great when I had those three steps under a single function. However, I decided to create a class and break those three steps into different methods. Now when I run the script, it only retrieves data from the first node in the list. I need some help figuring out why the script isn't working.

import concurrent.futures
import time
import telnetlib
from myFunctions import get_node_list_TA5K

class Ta5kTelnet:

    def __init__(self):
        self.tn = telnetlib.Telnet()

    def connect(self, hostname, username, password):
        self.tn.open(hostname, 23, 5)
        self.tn.read_until(b'Username:', 5)
        self.tn.write(username.encode('ascii') + b'\n')
        self.tn.read_until(b'Password:', 5)
        self.tn.write(password.encode('ascii') + b'\n')
        if b'>' in self.tn.read_until(b'>', 5):
            self.tn.write(b'en\n')
            self.tn.read_until(b'#', 5)
            self.tn.write(b'term len 0\n')
            output = self.tn.read_until(b'#', 5)
            return output
        else:
            print(f'{hostname} is spare shelf')

    def send(self, command, waitfor):
        self.tn.write(command + b'\n')
        result = self.tn.read_until(waitfor, 180).decode()
        return result
    
    def disconnect(self):
        self.tn.close()


tlnt = Ta5kTelnet()


def inventory(node_names):
    tlnt.connect(node_names, 'username', 'password')
    shelf_inventory = tlnt.send(b'show system inventory', b'#')
    tlnt.disconnect()
    with open(f'{node_names}_inventory.txt', 'a') as f:
        f.write(shelf_inventory)



#adtran_nodes = get_node_list_TA5K()
adtran_nodes = ['BXR1-NODE1-COT1', 'BXR6-NODE1-COT6']


start = time.perf_counter()

with concurrent.futures.ThreadPoolExecutor() as executor:
    results = executor.map(inventory, adtran_nodes)



print(time.perf_counter() - start)

Solution

  • You are sharing a single telnet connection across two threads, and there's no locking or synchronization.

    There's no earthly way of knowing which order that connect, login, send, read_until sequence will be run.

    Each thread needs its own connection object.

    In addition, I'd refactor things to derive from telnetlib.Telnet, and while at it, to ensure the connection gets closed correctly all the time, make it a context manager with __enter__ and __exit__:

    import concurrent.futures
    import telnetlib
    
    USERNAME = "username"
    PASSWORD = "password"
    
    
    class Ta5kTelnet(telnetlib.Telnet):
        def __enter__(self):
            return self
    
        def __exit__(self, *args):
            self.close()
    
        def login(self, hostname, username, password):
            self.open(hostname, 23, 5)
            self.read_until(b"Username:", 5)
            self.write(username.encode("ascii") + b"\n")
            self.read_until(b"Password:", 5)
            self.write(password.encode("ascii") + b"\n")
            if b">" in self.read_until(b">", 5):
                self.write(b"en\n")
                self.read_until(b"#", 5)
                self.write(b"term len 0\n")
                return self.read_until(b"#", 5)
            raise RuntimeError(f"{hostname} is spare shelf")
    
        def send_command(self, command, waitfor):
            self.write(command + b"\n")
            return self.read_until(waitfor, 180).decode()
    
    
    def get_node_inventory(node_name):
        with Ta5kTelnet() as tlnt:
            tlnt.login(node_name, USERNAME, PASSWORD)
            shelf_inventory = tlnt.send_command(b"show system inventory", b"#")
        with open(f"{node_name}_inventory.txt", "a") as f:
            f.write(shelf_inventory)
    
    
    adtran_nodes = ["BXR1-NODE1-COT1", "BXR6-NODE1-COT6"]
    
    with concurrent.futures.ThreadPoolExecutor() as executor:
        results = executor.map(get_node_inventory, adtran_nodes)