[Twisted-Python] SSHv2 client script for Cisco routers
Eli Criffield
elicriffield at gmail.com
Thu Jul 26 12:33:01 EDT 2007
In my tradition of answering my own questioins.
I finally got around to making a working ssh script that connects to
cisco routers and runs multiple commands.
It has to launch a real shell to work, then loop though the commands
waiting for the shell prompt to enter the next command. The problem i
was having before was i wasn't launching a real shell, just running a
command (among other problems). With a cisco there's no way to do
certain things except to enter a subshell. The script has to be a bit
"expect" like to accuplish that, but it doesn't make any disitions
based on output, just runs the given commands and returns the output
of all of them.
As a library you can run sshCmds() mulitple times. I work around the
problem having multiple reactors by forking for each sshCmds() call,
so it is its own process.
warning Big Cut&Paste
you can get it at: http://eli.criffield.net/conchClient.py too
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Eli Criffield < python AT zendo dot net >
#
# TODO: stderr is mixed in with stdout
# TODO: cmd line options should be more standard, and parsed by
# ConchOptions()
''' This will connect to a host and run a list of commands
Its different from just a normal connection because it launches a shell
and waits for the prompt, then sends the command, waits for prompt sends
command ect.. sshCmds() will return the output of all the commands it was
given together.
You probably want to run this on the command line or use sshCmds()
on the command line run:
conchClient.py host comand1 command2
in your python script use:
from conchClient import sshCmds
(out,err) = sshCmds(host,listOfCmds,user,echo)
print out
host is required
listOfCmds is any iterator that returns the commands
to be run (uswally a list)
user can be None, then the environment LOGNAME will be used
echo must be True or False
'''
__version__ = 0.1185457318
__author__ = 'Eli Criffield <ecriffield at fnni.com>'
import os
import re
import sys
import struct
import base64
from twisted.conch import error
from twisted.python import log
from twisted.conch.client import default, options
from twisted.conch.ssh import channel, common, connection, keys, session, \
transport, userauth
from twisted.internet import defer, protocol, reactor, stdio
# Regular expressions
rxPrompt = re.compile('[\\>\\#\\$] $', re.M)
rxMore = re.compile('--More--', re.M)
class SSHClientFactory(protocol.ClientFactory):
def __init__(self, cmdlist, host, user, outputer):
self.host = host
self.user = user
self.cmdlist = cmdlist
self.cmdlist.reverse()
self.outputer = outputer
def stopFactory(self):
try:
reactor.stop()
except:
pass
def buildProtocol(self, addr):
clientTransport = SSHClientTransport(cmdlist=self.cmdlist,
outputer=self.outputer)
clientTransport.user = self.user
clientTransport.host = self.host
return clientTransport
def clientConnectionFailed(self, connector, reason):
print 'Connection failed: %s' % reason
def clientConnectionLost(self, connector, reason):
pass
class ClientOptions(options.ConchOptions):
identitys = ['~/.ssh/id_rsa', '~/.ssh/id_dsa']
pass
class SSHClientUserAuth(default.SSHUserAuthClient):
def __init__(self, user, *args):
userauth.SSHUserAuthClient.__init__(self, user, *args)
self.keyAgent = None
self.options = ClientOptions()
self.usedFiles = []
class SSHClientConnection(connection.SSHConnection):
def __init__(self, cmdlist, outputer, *args, **kwargs):
connection.SSHConnection.__init__(self, *args, **kwargs)
self.cmdlist = cmdlist
self.outputer = outputer
def serviceStarted(self):
log.msg('Opening command channel')
self.openChannel(SSHCommandChannel(cmdlist=self.cmdlist,
outputer=self.outputer, conn=self))
class SSHClientTransport(transport.SSHClientTransport):
def __init__(self, cmdlist, outputer):
self.cmdlist = cmdlist
self.outputer = outputer
def receiveError(self, code, desc):
print 'disconnected error %i: %s' % (code, desc)
def sendDisconnect(self, code, reason):
print 'disconnect error %i: %s' % (code, reason)
transport.SSHClientTransport.sendDisconnect(self, code, reason)
def verifyHostKey(self, pubKey, fingerprint):
host = self.host
goodKey = default.isInKnownHosts(host, pubKey, {'known-hosts': None})
if goodKey == 1: # good key
return defer.succeed(1)
elif goodKey == 2: # AAHHHHH changed
return defer.fail(ConchError('changed host key'))
else:
(oldout, oldin) = (sys.stdout, sys.stdin)
sys.stdin = sys.stdout = open('/dev/tty', 'r+')
if host == self.transport.getPeer().host:
khHost = host
else:
host = '%s (%s)' % (host, self.transport.getPeer().host)
khHost = '%s,%s' % (host, self.transport.getPeer().host)
keyType = common.getNS(pubKey)[0]
print "The authenticity of host '%s' can't be
established.\n %s key fingerprint is %s." % \
(host, {'ssh-dss': 'DSA', 'ssh-rsa': 'RSA'}[keyType],
fingerprint)
try:
ans = raw_input('Are you sure you want to continue
connecting (yes/no)? ')
except KeyboardInterrupt:
return defer.fail(ConchError('^C'))
while ans.lower() not in ('yes', 'no'):
ans = raw_input("Please type 'yes' or 'no': ")
(sys.stdout, sys.stdin) = (oldout, oldin)
if ans == 'no':
print 'Host key verification failed.'
return defer.fail(ConchError('bad host key'))
print "Warning: Permanently added '%s' (%s) to the list of
known hosts." % \
(khHost, {'ssh-dss': 'DSA', 'ssh-rsa': 'RSA'}[keyType])
known_hosts = open(os.path.expanduser('~/.ssh/known_hosts'),
'r+')
known_hosts.seek(-1, 2)
if known_hosts.read(1) != '\n':
known_hosts.write('\n')
encodedKey = base64.encodestring(pubKey).replace('\n', '')
known_hosts.write('%s %s %s\n' % (khHost, keyType,
encodedKey))
known_hosts.close()
return defer.succeed(1)
def connectionSecure(self):
log.msg('Securing connection')
clientConnection = SSHClientConnection(cmdlist=self.cmdlist,
outputer=self.outputer)
self.requestService(SSHClientUserAuth(self.user,
clientConnection))
class SSHCommandChannel(channel.SSHChannel):
name = 'session'
def __init__(self, cmdlist, outputer, *args, **kwargs):
channel.SSHChannel.__init__(self, *args, **kwargs)
self.cmdlist = cmdlist
self.outputer = outputer
def openFailed(self, reason):
print 'channel open failed: %s' % reason
def channelOpen(self, data):
log.msg('Channel open')
term = 'ansi'
winsz = struct.pack('4H', 80, 25, 80, 25)
winSize = struct.unpack('4H', winsz)
ptyReqData = session.packRequest_pty_req(term, winSize, '')
self.conn.sendRequest(self, 'pty-req', ptyReqData)
self.conn.sendRequest(self, 'shell', '')
self.data = ''
def dataReceived(self, data):
self.data += data
self.outputer.moreStdout(data)
promptMatch = rxPrompt.search(self.data)
if promptMatch:
self.data = ''
if len(self.cmdlist) > 0:
cmd = self.cmdlist.pop()
log.msg('execing: %s'%cmd)
self.write(cmd)
self.write('\n')
else:
self.loseConnection()
# moreMatch is needed for cisco's
# Since we have a terminal, it pauses with a more
# at each screen, hit space and it will continue
moreMatch = rxMore.search(self.data)
if moreMatch:
self.data = ''
#hit space
self.write(' ')
def extReceived(self, t, data):
if t == connection.EXTENDED_DATA_STDERR:
sys.stderr.write(data)
def eofReceived(self):
log.msg('Received EOF')
self.loseConnection()
def closeReceived(self):
log.msg('Remote side closed')
self.conn.sendClose(self)
def closed(self):
log.msg('Channel closed')
reactor.stop()
log.msg('Reactor Stoped')
return None
def request_exit_status(self, data):
global exitStatus
exitStatus = int(struct.unpack('>L', data)[0])
log.msg('Exit status: %s' % exitStatus)
def sendEOF(self):
self.conn.sendEOF(self)
def stopWriting(self):
pass
def startWriting(self):
pass
class saveOutput:
def __init__(self, echo=False):
self.stdout = ''
self.stderr = ''
self.echo = echo
def moreStdout(self, data):
if self.echo:
sys.stdout.write(data)
sys.stdout.flush()
self.stdout += data
def moreStderr(self, data):
if self.echo:
sys.stderr.write(data)
sys.stderr.flush()
self.stderr += data
def __sshCmds(host, cmds, user=None, echo=False):
outputer = saveOutput(echo=echo)
clientFactory = SSHClientFactory(cmdlist=cmds, host=host, user=user,
outputer=outputer)
log.msg('Connecting to:%s'%host)
reactor.connectTCP(host, 22, clientFactory)
log.msg('Starting reactor')
reactor.run()
log.msg('Reactor has ended')
return (outputer.stdout, outputer.stderr)
def sshCmds(host, cmds, user=None, echo=False):
'''
from conchClient import sshCmds
(out,err) = sshCmds(host,listOfCmds,user,echo)
print out
host is required
listOfCmds is any interator that returns the commands
to be run (uswally a list)
user can be None, then the envorment LOGNAME will be used
echo must be True or False
'''
if user == None:
user = os.environ.get('LOGNAME')
# We have to fork because running the reactor twice doesn't work well
(rPipe, wPipe) = os.pipe() # these are file descriptors, not file objects
pid = os.fork()
if pid:
# I'm the Parent
os.close(wPipe)
rfd = os.fdopen(rPipe)
outpt = rfd.read()
os.waitpid(pid, 0) # make sure the child process gets cleaned up
# FIXME, stderr is mixed in with stdout
fakeerr = ''
return (outpt, fakeerr)
else:
# I'm the Child
os.close(rPipe)
wfd = os.fdopen(wPipe, 'w')
(stdout, stderr) = __sshCmds(host, cmds, user, echo)
# FIXME Stderr is mixed with stdout
wfd.write(stdout)
wfd.close()
sys.exit(0)
if __name__ == '__main__':
if len(sys.argv) < 3:
print '%s [user@]host cmd1 [cmd2 cmd3]'
sys.exit(1)
if (sys.argv)[1].find('@') != -1:
(user, host) = (sys.argv)[1].split('@', 1)
else:
user = None
host = (sys.argv)[1]
cmds = (sys.argv)[2:]
(out, err) = sshCmds(host, cmds, user, True)
print '\n'
More information about the Twisted-Python
mailing list