LinuxQuestions.org
Go Job Hunting at the LQ Job Marketplace
Go Back   LinuxQuestions.org > Blogs > Musings on technology, philosophy, and life in the corporate world
User Name
Password

Notices

Hi. I'm a Unix Administrator, mathematics enthusiast, and amateur philosopher. This is where I rant about that which upsets me, laugh about that which amuses me, and jabber about that which holds my interest most: Unix.
Rate this Entry

Remote management of Linux systems via Python and ssh (Part 2)

Posted 02-03-2009 at 08:44 PM by rocket357
Updated 10-27-2009 at 06:50 PM by rocket357 (modified code)

My last blogpost on Python/SSH seemed to be popular, so here I am again with a more in-depth look at what can be accomplished with this pairing.

To recap, I manage 300 databases, some M$ SQL Server, some PostgreSQL. There are a ton of Apache/Linux machines on our network and quite a few "traffic cops" based on Linux and BSD (as well as a few Cisco machines) for routing external traffic to internal machines. Pretty standard stuff. The IT department has a director, a network guru, a windows guru, a desktop part-timer who handles configuration and reinstallation of Windows desktops for the company, and a linux/database guru (me). Seeing as how I'm pulling double-duty, I had to figure out an easier way to manage all these machines.

So I found an SSH implementation in Python (paramiko) and a facade class (ssh.py) that runs on top of it. (A facade pattern is one that abstracts and simplifies underlying code). Using some basic python techniques, we can build a unix-ish script that can be executed directly from the command line, but run on tons of remote boxes.

No unix executable is complete without command-line arguments and options. So the first piece of the puzzle is optparse (link is to Python 2.5.2, which is the version I use at work). A basic optparse example looks like this (example taken from the above link):

Code:
from optparse import OptionParser
[...]
parser = OptionParser()
parser.add_option("-f", "--file", dest="filename",
                  help="write report to FILE", metavar="FILE")
parser.add_option("-q", "--quiet",
                  action="store_false", dest="verbose", default=True,
                  help="don't print status messages to stdout")

(options, args) = parser.parse_args()
With this basic bit of code, you can run:

<yourscript> -f output.txt --quiet

and yourscript will act just like a unix executable/script. You can run:

<yourscript> --help

and it'll print a simple usage (like most Linux/Unix scripts do).

Now, assuming you set up a similar "ssh master host" like I explained in this post, you can round-robin connect to each machine with something like this:

Code:
#!/usr/bin/env python
import os, sys, socket
from optparse import OptionParser
import ssh

parser = OptionParser()
parser.add_option("-t", "--timeout", dest="timeout", default=30)

# set a sane timeout for all connection attempts
try:
    socket.setdefaulttimeout(int(options.timeout))
except ValueError:
    print "Invalid input for timeout...using default"
    socket.setdefaulttimeout(30)

for host in range(1,256):
    try:
        connection = ssh.Connection("""192.168.1.%s""" % str(host))
        result = connection.execute("""uptime""")
        connection.close()
        print "192.168.1.%s: %s" % (str(host), result[0])
    except socket.error:
        print "192.168.1.%s connect FAILED!"
Ok, this code is simple and pretty straightforward. I add a timeout option so the user can dynamically set the timeout value on the command line like such:

<scriptname> -t 15 # sets a timeout of 15 for this script

Internally in the Python script you'd reference the -t 15 by "options.timeout", as you can see in the line:

socket.setdefaulttimeout(int(options.timeout))

(the int() part is needed to cast the string value passed in to an integer value for socket.setdefaulttimeout()). This script, then, will round-robin connect to each machine on the 192.168.1.0/24 subnet and print it's uptime.

Now, what if you want to set a longer timeout? At my place of employment we have some slower vpn connections to remote branches, and sometimes these connections can be quite sluggish. Unfortunately, I don't really want for each timeout to take forever. With this code being sequential (attempt to connect to this machine, wait, then attempt to connect to the next, wait, etc...) there is potential for the script to take a long time, and while one connection is active you can't interact with others. This is a minor annoyance unless the script needs to finish in a timely manner (maintenance window, for instance), or the script needs to connect to a very large number of private subnets.

The answer to this problem is threading. Now, many, many people will advise against threaded programming. It's a pain to manage, yes. It can get messy, yes. But for a simple "ssh connect and run a command" script, it'll allow multiple connections to take place simultaneously and be processed simultaneously. This has many advantages, but the main one is speed.

The simplest approach to threaded programming is to write a simple class that subclasses "Thread" from the threading package. Let's see an example:

Code:
#!/usr/bin/env python

import os, sys, socket
from threading import Thread
import ssh

class myThread(Thread):

    def __init__ (self, ip, command):
        Thread.__init__(self)
        self.ip = ip
        self.command = command

    def run (self):
        try:
            connection = ssh.Connection(self.ip)
            result = connection.execute(self.command)
            connection.close()
            print "%s connection success" % self.ip
            for line in result:
                print line
        except socket.error:
            print "%s connection FAILED!" % self.ip

    def test (self):
        pass # some code to test this class

if __name__ == "__main__":
    t1 = myThread()
    t1.test()
Now, there's a lot going on here...first we have the shebang line ( #!/usr/bin/env python ) that tells the Unix shell that this is a Python script, we have some standard imports (sys, os, OptionParser, ssh), and then we have a class definition ( class myThread(Thread): ). What the class definition does is declare a new class named "myThread" that subclasses "Thread". When a class subclasses another, it inherits all of the variables and functionality of the parent class (well, not exactly, but for simplicity's sake I'll leave it at that). The "Thread" parent class has a function named "start" that relies on a subclasses "run" function to actually launch the thread. The run function can only have one argument: self. Any setup that needs to be done should be done in the __init__ function after calling the parent class's __init__ function.

The last bit of the script demonstrates a standard "testing" method for python. When you run a script by name, it gets an Attribute "__name__" that contains "__main__". If you *import* the script into another script, the __name__ attribute is the name of the script that was imported. So, you can write a small if block like this:

Code:
if __name__ == "__main__":
    # commands to test stuff...
that will NOT get run when you import and execute the script, but it WILL get run if you call the script directly. This means you can integrate test code and not have to remove it for "production" runs =)

Ok, so how do we use this class? Let's see the "driver" script that will import and run this code:

Code:
#!/usr/bin/env python
import socket
from optparse import OptionParser
from thread_example import myThread

parser = OptionParser()
parser.add_option("-t", "--timeout", dest="timeout", default=30)
parser.add_option("-c", "--command", dest="command", default="uptime")

(options, args) = parser.parse_args()

# set a sane timeout for all connection attempts
try:
    socket.setdefaulttimeout(int(options.timeout))
except ValueError:
    print "Invalid input for timeout...using default"
    socket.setdefaulttimeout(30)

threadlist = []

for host in range(1,256):
    try:
        currentThread = myThread("192.168.1.%s" % str(host), options.command)
        threadlist.append(currentThread)
        currentThread.start()
    except:
        pass # default "catch-all" exception handler
and invoke it as such:

<driverscriptname> -t 15 -c "ls -lh"

to list your user directory on each machine, or:

<driverscriptname> -t 25 -c "uname -a"

Or you can utilize psycopg2 and pymssql (in addition to ssh.py) and run sql scripts to update all of your databases =D

More on that idea later...
Posted in Python
Views 4787 Comments 0
« Prev     Main     Next »
Total Comments 0

Comments

 

  



All times are GMT -5. The time now is 11:54 AM.

Main Menu
Advertisement

My LQ
Write for LQ
LinuxQuestions.org is looking for people interested in writing Editorials, Articles, Reviews, and more. If you'd like to contribute content, let us know.
Main Menu
Syndicate
RSS1  Latest Threads
RSS1  LQ News
Twitter: @linuxquestions
identi.ca: @linuxquestions
Facebook: linuxquestions Google+: linuxquestions
Open Source Consulting | Domain Registration