From f610fbf6ef97ab37ad1f313f15c3482204dab437 Mon Sep 17 00:00:00 2001 From: thierry Date: Mon, 29 Mar 2010 12:06:39 +0000 Subject: [PATCH] initial checkin of the vserver tools --- scripts/Makefile | 14 ++ scripts/VSHOSTS | 14 ++ scripts/vs-netcheck | 1 + scripts/vs-probe | 1 + scripts/vs-rename | 1 + scripts/vserver_tools.py | 374 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 405 insertions(+) create mode 100644 scripts/Makefile create mode 100644 scripts/VSHOSTS create mode 120000 scripts/vs-netcheck create mode 120000 scripts/vs-probe create mode 120000 scripts/vs-rename create mode 100755 scripts/vserver_tools.py diff --git a/scripts/Makefile b/scripts/Makefile new file mode 100644 index 0000000..abd3d3f --- /dev/null +++ b/scripts/Makefile @@ -0,0 +1,14 @@ +# $Id$ +# $URL$ + +vhosts: + cat VSHOSTS + +vprobe: + vs-probe -f VSHOSTS > VSERVERS + @echo 'refreshed VSERVERS' + cat VSERVERS + +vcheck: + vs-netcheck -f VSERVERS + diff --git a/scripts/VSHOSTS b/scripts/VSHOSTS new file mode 100644 index 0000000..b8f0fa1 --- /dev/null +++ b/scripts/VSHOSTS @@ -0,0 +1,14 @@ +#blitz.pl.sophia.inria.fr +#liquid.pl.sophia.inria.fr +#reed.pl.sophia.inria.fr +#velvet.pl.sophia.inria.fr +warhol.pl.sophia.inria.fr +speedball.pl.sophia.inria.fr +addntox.pl.sophia.inria.fr +deathvegas.pl.sophia.inria.fr +gorillaz.pl.sophia.inria.fr +gotan.pl.sophia.inria.fr +truckers.pl.sophia.inria.fr + + + diff --git a/scripts/vs-netcheck b/scripts/vs-netcheck new file mode 120000 index 0000000..a7d826c --- /dev/null +++ b/scripts/vs-netcheck @@ -0,0 +1 @@ +vserver_tools.py \ No newline at end of file diff --git a/scripts/vs-probe b/scripts/vs-probe new file mode 120000 index 0000000..a7d826c --- /dev/null +++ b/scripts/vs-probe @@ -0,0 +1 @@ +vserver_tools.py \ No newline at end of file diff --git a/scripts/vs-rename b/scripts/vs-rename new file mode 120000 index 0000000..a7d826c --- /dev/null +++ b/scripts/vs-rename @@ -0,0 +1 @@ +vserver_tools.py \ No newline at end of file diff --git a/scripts/vserver_tools.py b/scripts/vserver_tools.py new file mode 100755 index 0000000..27874c8 --- /dev/null +++ b/scripts/vserver_tools.py @@ -0,0 +1,374 @@ +#!/usr/bin/python +# +# $Id$ +# $URL$ + +import sys +import os.path +import pdb +import subprocess +from optparse import OptionParser +import socket + +# +# Thierry Parmentelat - INRIA +# +# class for issuing commands on a box, either local or remote +# +# the notion of 'buildname' is for providing each test run with a dir of its own +# buildname is generally the name of the build being tested, and can be considered unique +# +# thus 'run_in_buildname' mostly : +# (*) either runs locally in . - as on a local node we are already in a dedicated directory +# (*) or makes sure that there's a remote dir called 'buildname' and runs in it +# +# also, the copy operations +# (*) either do nothing if ran locally +# (*) or copy a local file into the remote 'buildname' +# + +######################################## +class Logger: + + def __init__(self): + pass + + def log (self,msg): + print >>sys.stderr, msg + +logger=Logger() + +######################################## +# bool_to_exit_code +def b2e (b): + if b: return 0 + else: return 1 +# exit_to_bool_code +def e2b (b): + return b==0 + +verbose=False +verbose=True +######################################## +class Shell: + def __init__(self,dry_run=False): + self.dry_run=dry_run + + def header (self,message): + logger.log("==============="+message) + + # return an exit code + def run (self, argv, trash_stdout=False, trash_stderr=False, message="", force_dry_run=False): + if self.dry_run: + print 'DRY_RUN',message+'RUNNING '+" ".join(argv) + return 0 + else: + if message: self.header(message), + if verbose: logger.log ('RUNNING '+" ".join(argv)) + if trash_stdout and trash_stderr: + return subprocess.call(argv,stdout=file('/dev/null','w'),stderr=subprocess.STDOUT) + elif trash_stdout and not trash_stderr: + return subprocess.call(argv,stdout=file('/dev/null','w')) + elif not trash_stdout and trash_stderr: + return subprocess.call(argv,stderr=file('/dev/null','w')) + elif not trash_stdout and not trash_stderr: + return subprocess.call(argv) + + # like shell's $(...) which used to be `...` + def backquote (self, argv, trash_stderr=False,message=""): + if self.dry_run or verbose: + if verbose: logger.log("WARNING: " + message + " backquote is actually running "+" ".join(argv)) + if not trash_stderr: + return subprocess.Popen(argv,stdout=subprocess.PIPE).communicate()[0] + else: + return subprocess.Popen(argv,stdout=subprocess.PIPE,stderr=file('/dev/null','w')).communicate()[0] + +#################### +class Ssh: + + #################### the class + def __init__(self,hostname=None,key=None, username='root',dry_run=False): + self.hostname=hostname + if not hostname: self.hostname='localhost' + self.key=key + self.username=username + self.dry_run=dry_run + + # inserts a backslash before each occurence of the following chars + # \ " ' < > & | ; ( ) $ * ~ + @staticmethod + def backslash_shell_specials (command): + result='' + for char in command: + if char in "\\\"'<>&|;()$*~": + result +='\\'+char + else: + result +=char + return result + + # check main IP address against the provided hostname + @staticmethod + def is_local_hostname (hostname): + if hostname == "localhost": + return True + try: + local_ip = socket.gethostbyname(socket.gethostname()) + remote_ip = socket.gethostbyname(hostname) + return local_ip==remote_ip + except: + logger.log("WARNING : something wrong in is_local_hostname with hostname=%s"%hostname) + return False + + @staticmethod + def canonical_name (hostname): + try: + ip=socket.gethostbyname(hostname) + return socket.gethostbyaddr(ip)[0] + except: + logger.log( 'Warning - could not find canonical name for host %s'%hostname) + return hostname + + + ########## building argv + def is_local(self): + return Ssh.is_local_hostname(self.hostname) + + def key_argv (self): + if not self.key: + return [] + return ["-i",self.key] + + def hostname_arg (self): + if not self.username: + return self.hostname + else: + return "%s@%s"%(self.username,self.hostname) + + std_options="-o BatchMode=yes -o StrictHostKeyChecking=no -o CheckHostIP=no -o ConnectTimeout=5" + + # command gets run on the right box + def argv (self, argv, keep_stdin=False): + if self.is_local(): + return argv + ssh_argv = ["ssh"] + if not keep_stdin: ssh_argv.append('-n') + ssh_argv += Ssh.std_options.split() + ssh_argv += self.key_argv() + ssh_argv.append (self.hostname_arg()) + # using subprocess removes the need for special chars at this point + #ssh_argv += [Ssh.backslash_shell_specials(x) for x in argv] + ssh_argv += argv + return ssh_argv + + # argv is a list + def run(self, argv,*args, **kwds): + return Shell(self.dry_run).run(self.argv(argv),*args, **kwds) + # command is a string + def run_string(self, command, *args, **kwds): + return Shell(self.dry_run).run(self.argv(command.split()),*args, **kwds) + + # argv is a list + def backquote(self, argv, *args, **kwds): + return Shell(self.dry_run).backquote(self.argv(argv),*args, **kwds) + # command is a string + def backquote_string(self, command, *args, **kwds): + return Shell(self.dry_run).backquote(self.argv(command.split()),*args, **kwds) + +############################################################ +class Vserver: + + def __init__ (self, name, dry_run=False): + try: + (self.name,self.hostname)=name.split("@") + except: + (self.name,self.hostname)=(name,'localhost') + self.dry_run=dry_run + self.ssh=Ssh(self.hostname,username='root',dry_run=self.dry_run) + self._is_running=None + + # cache run mode + def is_running (self): + if self._is_running is not None: return self._is_running + self._is_running = e2b(self.ssh.run_string('vserver %s status'%self.name, + trash_stdout=True,trash_stderr=True, + message='retrieving status for %s'%self.name)) + def running_char (self): + if self._is_running is None: return '?' + elif not self._is_running: return '-' + else: return '+' + + def stop (self): + # uncache + self._is_running=None + return e2b(self.ssh.run_string("vserver %s stop"%self.name,message='Stopping %s'%self.name)) + + def start (self): + # uncache + self._is_running=None + return e2b(self.ssh.run_string("vserver %s start"%self.name,message='Starting %s'%self.name)) + + def printable_simple(self): return self.name + '@' + self.hostname + def printable_dbg(self): return "%s@%s (%s)"%(self.name,self.hostname,self.running_char()) + def printable(self): + # force fetch + self.is_running() + return "%s@%s (%s)"%(self.name,self.hostname,self.running_char()) + + def is_local (self): + return self.ssh.is_local() + + @staticmethod + def probe_host (hostname, dry_run=False): + canonical=Ssh.canonical_name(hostname) + ssh=Ssh(hostname,dry_run=dry_run) + ls=ssh.backquote_string("ls -d1 /vservers/*",trash_stderr=True) + return [ '%s@%s'%(path.replace('/vservers/',''),canonical) for path in ls.split()] + + # renaming + def rename (self, newname): + target=Vserver(newname) + if self.hostname != target.hostname: + # make user's like easier, no need to mention hostname again + if target.is_local(): + target.hostname=self.hostname + else: + logger.log('rename cannot rename (%s->%s) - must be on same box'%(self.hostname, target.hostname)) + return False + logger.log("Renaming %s into %s"%(self.printable(),target.printable_simple())) + + ssh=self.ssh + oldname=self.name + newname=target.name + xid=ssh.backquote(['cat','/etc/vservers/%(oldname)s/context'%locals()]) + currently_running=self.is_running() + result=True + if currently_running: + result = result and self.stop() + result = result \ + and e2b(ssh.run_string("mv /vservers/%(oldname)s /vservers/%(newname)s"%locals())) \ + and e2b(ssh.run_string("mv /etc/vservers/%(oldname)s /etc/vservers/%(newname)s"%locals())) \ + and e2b(ssh.run_string("rm /etc/vservers/%(newname)s/run"%locals())) \ + and e2b(ssh.run_string("ln -s /var/run/vservers/%(newname)s /etc/vservers/%(newname)s/run"%locals())) \ + and e2b(ssh.run_string("rm /etc/vservers/%(newname)s/vdir"%locals())) \ + and e2b(ssh.run_string("ln -s /etc/vservers/.defaults/vdirbase/%(newname)s /etc/vservers/%(newname)s/vdir"%locals())) \ + and e2b(ssh.run_string("rm /etc/vservers/%(newname)s/cache"%locals())) \ + and e2b(ssh.run_string("ln -s /etc/vservers/.defaults/cachebase/%(newname)s /etc/vservers/%(newname)s/cache"%locals())) \ + and e2b(ssh.run_string("rm /var/run/vservers.rev/%(xid)s"%locals())) \ + and e2b(ssh.run_string("ln -s /etc/vserver/%(newname)s /var/run/vservers.rev/%(xid)s"%locals())) + if currently_running: + result = result and self.start() + return result + + def ok(self,message): logger.log('OK %s: %s'%(self.printable(),message)) + def error(self,message): logger.log('ERROR %s: %s'%(self.printable(),message)) + def warning(self,message): logger.log( 'WARNING %s: %s'%(self.printable(),message)) + + def netcheck(self): + result=True + ssh=self.ssh + name=self.name + hostname=ssh.backquote_string("cat /etc/vservers/%(name)s/uts/nodename"%locals()).strip() + if not hostname: + self.error('no uts/nodename') + result=False + ip=ssh.backquote_string("cat /etc/vservers/%(name)s/interfaces/0/ip"%locals()).strip() + if not ip: + self.error('no interfaces/0/ip') + result=False + try: + expected_ip=socket.gethostbyname(hostname) + if expected_ip == ip: + self.ok('hostname %s maps to IP %s'%(hostname,ip)) + else: + self.error('IP mismatch, current %s -- expected %s'%(ip,expected_ip)) + result=False + except: + self.warning ('could not resolve hostname <%s>'%hostname) + expected_ip='255.255.255.255' + result=False + mask_path="/etc/vservers/%(name)s/interfaces/mask"%locals() + mask_exists = e2b(ssh.run_string('ls %s'%mask_path,trash_stderr=True)) + if mask_exists: + self.error('found a mask file in %s'%(self.printable(),mask_path)) + result=False + else: + self.ok('no mask file %s'%mask_path) + return result + +class Main: + + modes={ + # expected arg numbers can be either + # negative int, e.g. -1 means at least one arg + # positive int, exactly that number of args + # list of ints : allowed number of args + 'probe' : "hostname(s)\n\tprobes all hostnames and returns the found vservers", + 'netcheck' : "vsname\n\tchecks the network config for a vserver", + 'rename' : "oldname newname\n\trename a vserver - both named must be local", + 'migrate' : "oldname hostname@newname\n\tmigrate a vserver over to a new box; must be run on the current box\n\tnot implemented yet", + } + + def read_args_from_file (self,args_from_file): + if not args_from_file: return [] + try: + raw=file(args_from_file).read().split() + return [x for x in raw if x.find('#')<0] + except: + logger.log('ERROR: Could not open args file %s'%args_from_file) + return [] + + def run (self): + + mode=None + for function in Main.modes.keys(): + if sys.argv[0].find(function) >= 0: + mode = function + mode_desc = Main.modes[mode] + break + if not mode: + print "Unsupported command",sys.argv[0] + print "Supported commands:" + " ".join(Main.modes.keys()) + sys.exit(1) + + parser=OptionParser(usage="%prog " + mode_desc) + parser.add_option('-n','--dry-run',dest='dry_run',action='store_true',default=False, + help="dry run") + if mode == 'probe' or mode == 'netcheck': + parser.add_option ('-f','--file',action='store',dest='args_from_file',default=None, + help='read args from file') + + (options,args) = parser.parse_args() + parser.set_usage("%prog " + mode_desc) + + # check number of arguments + def bail_out (msg): + parser.print_help(msg) + sys.exit(1) + + # + if mode == 'probe': + names=[] + if not options.args_from_file and len(args)==0: bail_out('no arg specified') + for arg in args + self.read_args_from_file(options.args_from_file): + names += Vserver.probe_host(arg,options.dry_run) + names.sort() + for name in names: print name + + elif mode=='netcheck': + result=True + if not options.args_from_file and len(args)==0: bail_out('no arg specified') + for arg in args + self.read_args_from_file(options.args_from_file): + if not b2e(Vserver(arg,options.dry_run).netcheck()): result=False + return result + elif mode=='rename': + [x,y]=args + return b2e(Vserver(x,options.dry_run).rename(y)) + elif mode=='migrate': + print 'migrate not yet implemented' + return 1 + else: + print 'unknown mode %s'%mode + return b2e(False) + +if __name__ == '__main__': + sys.exit( Main().run() ) -- 2.43.0