Python, Telnet and Speedtouch

I wrote a utility called telscript that can be used to send scripted commands over telnet. I wrote it specifically to send commands to a SpeedTouch 585v7 ADSL Router. It makes use of the Python telnetlib module.

Because it expects specific responses from the router, it may not be suitable for other uses without modification.

Telscript works unmodified with Python versions 2 and 3.

Telnetlib and Python 3

I managed to retain compatibility with Python versions 2 and 3 by using syntax compatible with both versions. The main changes were

  • change print statments to functions (add parentheses)
  • use as when capturing errors with except
  • the change of the str type to Unicode

Relaunch with Python 2

Before Telnetlib worked with Python 3, I modified the code so it was valid syntax for both Python 2 and 3 and added code to detect the version and restart with Python 2 if launched with Python 3:

if sys.version > '3':
    python2 = os.popen('which python2 2> /dev/null').read().rstrip()
    if python2:
      args = sys.argv[:]
      args.insert(0,python2)
      os.execv(python2,args)
    else:
      abort("%s requires Python Version 2 (python2 not in PATH)" % os.path.basename(__file__))

This trick is based on something similar by Ahmed Soliman. It was removed once I had full Python 3 compatibility because it was no longer necessary to restart with Python 2.

When a String is no longer a String

One of the changes between Python versions 2 and 3 is the introduction of Unicode strings and Telnetlib doesn't work with Unicode strings.

The solution to this problem is to subclass telnetlib.Telnet to override its methods and perform the required conversions.

A Super Difference

Python 2 has old-style and new-style classes, the difference being that old-style classes aren't a subclass of Object wheras new-style classes, and all Python 3 classes, are.

One difference this brings is that old-style classes don't have the super method used to execute a method in the superclass. You can work around this in Python 2, either by including Object in class definitions and then using super, the preferred form that also works in Python 3:

class Telnet(telnetlib.Telnet,object):
    def read_until(self,expected,timeout=None):
        return super(Telnet,self).read_until(expected,timeout)

Or, a similar effect cna be achieved with old-style Python 2 classes:

class Telnet(telnetlib.Telnet):
    def read_until(self,expected,timeout=None):
        return telnetlib.Telnet.read_until(self,expected,timeout)

(I used Object and super because I wanted to use Python 3 also)

Interactive Mode

Although its primary purpose is to run command scripts into the router's telnet interface, Telscript also has an interactive mode that is invoked when its standard input is a terminal.

It is, however, a little quirky due to the interactive capabilities of the underlying telnetlib module. I wanted to implement it like this:

if input.isatty():
    tn.interact()
else:
    ... scripted mode...

This works, except that it's necessary to send an escaped line-feed to the router: you need to press Control-V before pressing ENTER twice. This may be a router quirk but it makes it rather unusable.

I then implemented interactive mode using a read/write loop that reads a line of input from the terminal, sends it to the router and then reads and displays its response:

print("Started interactive session on %s\n%s" % (hostname,match_text)),
while True: 
    tn.write(input.readline() + "\r")
    try:
        print(tn.read_until('{admin}=>')),
    except EOFError:
        break  

This works much better, but the EOF due to the connection being closed by issuing the router's exit command isn't detected until after attempting to send another write. This means that a second hit of the ENTER key is required to exit the interactive session.

I then implemented it differently, sending one character at a time and reading responses until it blocks:

while True:
    c = input.read(1)
    tn.write(c)                            
    try:
        print(tn.read_very_eager()),
    except EOFError:
        break

However this still has the end of line problem but is worse. In the end, I decided to use the line-at-once method and live with the need for an additional ENTER after exit. I hope, however, to find a better solution.

Suppressing echo

It's usual for the remote telnet server to echo back the characters it receives. If it supports it (the Speedtouch doesn't appear to) then you can request it not to. Add a method to the Telnet class:

def echo_off(self):
    self.get_socket().send(telnetlib.IAC + telnetlib.DONT + telnetlib.ECHO)
    self.get_socket().send(telnetlib.IAC + telnetlib.WILL + telnetlib.ECHO)

and the use it like this:

tn.echo_off()

Interactive Prompt (Python print hack)

I wanted the cursor to be left beside the prompt. This required that the print of the prompt should not be followed by a newline. Python 2 and 3 have different syntax for this:

print(s),

whereas Python 3 does this:

print(s, end='')

I tried using the sys.version > '3' version check to use one or the other forms but the problem is that these are syntax differences and the syntax has to be correct otherwise the program won't run, even though invalid code would never be executed.

I thought I could fix it by using exec to execute a variable containing the relevant code, which gets set according to the same sys.version > '3' condition.

p = "print(s, end='')" if sys.version > '3' else "print(s),"
def prompt(s):
    exec p in  globals(), locals()

The problem is just shifted because the syntax of exec has also changed between versions 2 and 3. In Python 3 it needs to be like this:

exec(p, globals(), locals())

So, I gave up. When using Python 3 to run Telscript interactively, the cursor will appear on the line beneath the prompt. When using Python 2, the cursor appears alongside the prompt. Live with it!