initial checkin of the vserver tools
authorthierry <thierry@41d37cc5-eb28-0410-a9bf-d37491348ade>
Mon, 29 Mar 2010 12:06:39 +0000 (12:06 +0000)
committerthierry <thierry@41d37cc5-eb28-0410-a9bf-d37491348ade>
Mon, 29 Mar 2010 12:06:39 +0000 (12:06 +0000)
scripts/Makefile [new file with mode: 0644]
scripts/VSHOSTS [new file with mode: 0644]
scripts/vs-netcheck [new symlink]
scripts/vs-probe [new symlink]
scripts/vs-rename [new symlink]
scripts/vserver_tools.py [new file with mode: 0755]

diff --git a/scripts/Makefile b/scripts/Makefile
new file mode 100644 (file)
index 0000000..abd3d3f
--- /dev/null
@@ -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 (file)
index 0000000..b8f0fa1
--- /dev/null
@@ -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 (symlink)
index 0000000..a7d826c
--- /dev/null
@@ -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 (symlink)
index 0000000..a7d826c
--- /dev/null
@@ -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 (symlink)
index 0000000..a7d826c
--- /dev/null
@@ -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 (executable)
index 0000000..27874c8
--- /dev/null
@@ -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() )