From fb752dbac8f142946657f8c1c264776f61df5c20 Mon Sep 17 00:00:00 2001 From: thierry Date: Tue, 30 Mar 2010 08:33:32 +0000 Subject: [PATCH] first working draft - migrate not implemented yet --- scripts/Makefile | 16 +- scripts/VSERVERS | 11 + scripts/VSHOSTS | 14 -- scripts/{vs-netcheck => vs-check} | 0 scripts/{vs-probe => vs-show} | 0 scripts/vserver_tools.py | 388 +++++++++++++++++++----------- 6 files changed, 263 insertions(+), 166 deletions(-) create mode 100644 scripts/VSERVERS delete mode 100644 scripts/VSHOSTS rename scripts/{vs-netcheck => vs-check} (100%) rename scripts/{vs-probe => vs-show} (100%) diff --git a/scripts/Makefile b/scripts/Makefile index abd3d3f..b16ab47 100644 --- a/scripts/Makefile +++ b/scripts/Makefile @@ -1,14 +1,14 @@ # $Id$ # $URL$ -vhosts: - cat VSHOSTS +vcheck: + vs-check -f VSERVERS -vprobe: - vs-probe -f VSHOSTS > VSERVERS - @echo 'refreshed VSERVERS' - cat VSERVERS +vshow: + vs-show -l -f VSERVERS -vcheck: - vs-netcheck -f VSERVERS +vcache: + vs-show -f VSERVERS > VCACHE +vshowcache: + vs-show -l -f VCACHE diff --git a/scripts/VSERVERS b/scripts/VSERVERS new file mode 100644 index 0000000..cbd4a3f --- /dev/null +++ b/scripts/VSERVERS @@ -0,0 +1,11 @@ +#*@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/VSHOSTS b/scripts/VSHOSTS deleted file mode 100644 index b8f0fa1..0000000 --- a/scripts/VSHOSTS +++ /dev/null @@ -1,14 +0,0 @@ -#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-check similarity index 100% rename from scripts/vs-netcheck rename to scripts/vs-check diff --git a/scripts/vs-probe b/scripts/vs-show similarity index 100% rename from scripts/vs-probe rename to scripts/vs-show diff --git a/scripts/vserver_tools.py b/scripts/vserver_tools.py index 27874c8..d1a8c83 100755 --- a/scripts/vserver_tools.py +++ b/scripts/vserver_tools.py @@ -33,7 +33,8 @@ class Logger: def __init__(self): pass - def log (self,msg): + def log (self,msg,level=0): + if level>0: print >> sys.stderr,level*4*'=', print >>sys.stderr, msg logger=Logger() @@ -47,24 +48,36 @@ def b2e (b): def e2b (b): return b==0 -verbose=False -verbose=True ######################################## class Shell: - def __init__(self,dry_run=False): + def __init__(self,dry_run=False,verbose=False): self.dry_run=dry_run + self.verbose=verbose def header (self,message): - logger.log("==============="+message) + logger.log(message,level=1) + + # convenience : if argv is a string, we just split it to get a list + # if args may contain spaces, it's safer to build the list yourself + def normalize (self, argv): + if isinstance (argv,list): return argv + if isinstance (argv,str): return argv.split() + else: raise Exception, "Unsupported command type in Shell - %r"%argv # return an exit code + # in general dry_run ... does not run + # there are cases where the command is harmless and required for the rest to proceed correctly + # in this case you can set force_dry_run 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) + argv=self.normalize(argv) + if self.verbose and message: self.header(message), + if self.dry_run and not force_dry_run: + print 'WOULD RUN '+" ".join(argv) return 0 else: - if message: self.header(message), - if verbose: logger.log ('RUNNING '+" ".join(argv)) + helper='RUNNING' + if force_dry_run and self.verbose: helper += ' (forced)' + if self.verbose: logger.log (helper+' '+" ".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: @@ -74,25 +87,29 @@ class Shell: 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)) + # like shell's $(...) which used to be `...` - cannot trash stdout of course + # ditto for the dry_run mode + def backquote (self, argv, trash_stderr=False,message="", force_dry_run=False): + argv=self.normalize (argv) + if self.verbose and message: self.header(message), + if self.dry_run and not force_dry_run: + print 'WOULD RUN $('+" ".join(argv) + ')' + return "" 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: +class Ssh (Shell): #################### the class - def __init__(self,hostname=None,key=None, username='root',dry_run=False): + def __init__(self,hostname=None,key=None, username='root',dry_run=False,verbose=False): self.hostname=hostname if not hostname: self.hostname='localhost' self.key=key self.username=username - self.dry_run=dry_run + Shell.__init__(self,dry_run=dry_run,verbose=verbose) # inserts a backslash before each occurence of the following chars # \ " ' < > & | ; ( ) $ * ~ @@ -125,7 +142,7 @@ class Ssh: ip=socket.gethostbyname(hostname) return socket.gethostbyaddr(ip)[0] except: - logger.log( 'Warning - could not find canonical name for host %s'%hostname) + logger.log( "WARNING - could not find canonical name for host %s"%hostname) return hostname @@ -160,118 +177,124 @@ class Ssh: 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 + return Shell.run(self,self.argv(self.normalize(argv)),*args, **kwds) 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) + return Shell.backquote(self,self.argv(self.normalize(argv)),*args, **kwds) ############################################################ class Vserver: - def __init__ (self, name, dry_run=False): + def __init__ (self, name, dry_run=False,verbose=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 + self.verbose=verbose + self._running_state=None + self._xid=None + self._autostart=None + self.set_hostname(self.hostname) + + def set_hostname (self, hostname): + self.hostname=hostname + self.ssh=Ssh(self.hostname,username='root',dry_run=self.dry_run,verbose=self.verbose) + + def ok(self,message): + if self.verbose: logger.log('OK %s: %s'%(self.printable(),message)) + def warning(self,message): + if self.verbose: logger.log( 'WARNING %s: %s'%(self.printable(),message)) + def error(self,message): + logger.log('ERROR %s: %s'%(self.printable(),message)) # 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_state (self): + if self._running_state is not None: return self._running_state + status_retcod= self.ssh.run('vserver %s status'%self.name, + trash_stdout=True,trash_stderr=True, + message='retrieving status for %s'%self.name, + force_dry_run=True) + if status_retcod == 0: self._running_state = 'start' + elif status_retcod == 3: self._running_state = 'stop' + else: self._running_state = 'unknown' + return self._running_state + def running_char (self): - if self._is_running is None: return '?' - elif not self._is_running: return '-' - else: return '+' + if self._running_state is None: return '?' + elif self._running_state == 'unknown' : return '!' + elif self._running_state == 'stop': return '-' + else: return '+' + + # cache xid + def xid (self): + if self._xid is not None: return self._xid + self._xid = self.ssh.backquote(['cat','/etc/vservers/%s/context'%self.name], + force_dry_run=True,trash_stderr=True, + message='Retrieving xid for %s'%self.name).strip() + return self._xid + + def autostart (self): + if self._autostart is not None: return self._autostart + self._autostart = self.ssh.backquote(['cat','/etc/vservers/%s/apps/init/mark'%self.name], + force_dry_run=True,trash_stderr=True, + message='Retrieving autostart status for %s'%self.name).strip()=='default' + return self._autostart def stop (self): # uncache - self._is_running=None - return e2b(self.ssh.run_string("vserver %s stop"%self.name,message='Stopping %s'%self.name)) + self._running_state=None + return e2b(self.ssh.run("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)) + self._running_state=None + return e2b(self.ssh.run("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_dbg(self): + autostart_part='<*>' + if not self.autostart(): autostart_part='< >' + return autostart_part+" (%s) [%s] %s@%s"%(self.running_char(),self.xid(),self.name,self.hostname) def printable(self): - # force fetch - self.is_running() - return "%s@%s (%s)"%(self.name,self.hostname,self.running_char()) + # force fetch cache info + self.running_state() + self.xid() + self.autostart() + return self.printable_dbg() 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)) + # does it have a pattern + def has_pattern (self): + return self.name.find('*') >= 0 + + # probe it + def probe_pattern (self): + pattern=self.name + ls=self.ssh.backquote("ls -d1 /vservers/%s"%pattern, + trash_stderr=True,force_dry_run=True, + message='scanning /vservers with pattern %s on %s'%(pattern,self.hostname)) + return [ '%s@%s'%(path.replace('/vservers/',''),self.hostname) for path in ls.split()] + + #################### toplevel methods for main + def show (self, long_format): + if long_format: + print self.printable() + else: + print self.printable_simple() - def netcheck(self): + def check(self): result=True ssh=self.ssh name=self.name - hostname=ssh.backquote_string("cat /etc/vservers/%(name)s/uts/nodename"%locals()).strip() + hostname=ssh.backquote("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() + self.warning('no uts/nodename') + ip=ssh.backquote("cat /etc/vservers/%(name)s/interfaces/0/ip"%locals()).strip() if not ip: self.error('no interfaces/0/ip') result=False @@ -287,7 +310,7 @@ class Vserver: 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)) + mask_exists = e2b(ssh.run('ls %s'%mask_path,trash_stderr=True)) if mask_exists: self.error('found a mask file in %s'%(self.printable(),mask_path)) result=False @@ -295,74 +318,151 @@ class Vserver: self.ok('no mask file %s'%mask_path) return result -class Main: + # 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('ERROR:rename cannot rename (%s->%s) - must be on same box'%(self.hostname, target.hostname)) + return False + logger.log("%s into %s"%(self.printable(),target.printable_simple()),level=2) + + ssh=self.ssh + oldname=self.name + newname=target.name + xid=self.xid() + currently_running=self.running_state()=='start' + result=True + if currently_running: + result = result and self.stop() + result = result \ + and e2b(ssh.run("mv /vservers/%(oldname)s /vservers/%(newname)s"%locals(), + message="tweaking /vservers/<>")) \ + and e2b(ssh.run("mv /etc/vservers/%(oldname)s /etc/vservers/%(newname)s"%locals(), + message="tweaking /etc/vservers/<>")) \ + and e2b(ssh.run("rm /etc/vservers/%(newname)s/run"%locals(), + message="cleaning /etc/vservers/<>/run")) \ + and e2b(ssh.run("ln -s /var/run/vservers/%(newname)s /etc/vservers/%(newname)s/run"%locals(), + message="adjusting /etc/vservers/<>/run")) \ + and e2b(ssh.run("rm /etc/vservers/%(newname)s/vdir"%locals(), + message="cleaning /etc/vservers/<>/vdir")) \ + and e2b(ssh.run("ln -s /etc/vservers/.defaults/vdirbase/%(newname)s /etc/vservers/%(newname)s/vdir"%locals(), + message="adjusting /etc/vservers/<>/vdir")) \ + and e2b(ssh.run("rm /etc/vservers/%(newname)s/cache"%locals(), + message="cleaning /etc/vservers/<>/cache")) \ + and e2b(ssh.run("ln -s /etc/vservers/.defaults/cachebase/%(newname)s /etc/vservers/%(newname)s/cache"%locals(), + message="adjusting /etc/vservers/<>/cache")) \ + and e2b(ssh.run("rm /var/run/vservers.rev/%(xid)s"%locals(), + message="cleaning /var/run/vservers.rev/<>")) \ + and e2b(ssh.run("ln -s /etc/vserver/%(newname)s /var/run/vservers.rev/%(xid)s"%locals(), + message="adjusting /var/run/vservers.rev/<>")) +# # refreshing target instance + target=Vserver(newname,dry_run=self.dry_run,verbose=self.verbose) + target.set_hostname(self.hostname) + if currently_running and not self.dry_run: + result = result and target.start() + print target.printable() + return result + +class VserverTools: 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", + 'show' : "vsname(s)\n\tdisplay status & details", + 'start': "vsname(s)\n\tstart vservers", + 'stop' : "vsname(s)\n\tstop vservers", + 'check' : "vsname(s)\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", + 'migrate' : "oldname hostname@newname\n\tmigrate a vserver over to a new box\n\tnot implemented yet", } - def read_args_from_file (self,args_from_file): - if not args_from_file: return [] + def __init__ (self): + self.parser=OptionParser() + self.args=None + self.options=None + + def vserver(self, vsname): + return Vserver(vsname,dry_run=self.options.dry_run,verbose=self.options.verbose) + + def probe_star_arg (self, arg): + vs=self.vserver(arg) + if vs.has_pattern(): return vs.probe_pattern() + else: return [arg,] + + def read_args_from_file (self): + if not self.options.args_from_file: return [] try: - raw=file(args_from_file).read().split() - return [x for x in raw if x.find('#')<0] + result=[] + raw_args=file(self.options.args_from_file).read().split() + for arg in raw_args: + result += self.probe_star_arg(arg) + return result except: - logger.log('ERROR: Could not open args file %s'%args_from_file) + logger.log('ERROR: Could not open args file %s'%self.options.args_from_file) return [] + def check_usual_args(self): + if not self.options.args_from_file and len(self.args)==0: + print ('no arg specified') + self.parser.print_help() + sys.exit(1) + result=[] + for arg in self.args: result += self.probe_star_arg(arg) + result += self.read_args_from_file() + result.sort() + return result + + # return exit code def run (self): mode=None - for function in Main.modes.keys(): + for function in VserverTools.modes.keys(): if sys.argv[0].find(function) >= 0: mode = function - mode_desc = Main.modes[mode] + mode_desc = VserverTools.modes[mode] break if not mode: print "Unsupported command",sys.argv[0] - print "Supported commands:" + " ".join(Main.modes.keys()) + print "Supported commands:" + " ".join(VserverTools.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, + self.parser.set_usage("%prog " + mode_desc) + self.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': + self.parser.add_option('-v','--verbose',dest='verbose',action='store_true',default=False, + help="be verbose") + self.parser.add_option ('-l','--long',action='store_true', dest='long_format',default=False, + help="show more detailed result") + ########## + if mode not in ['rename','migrate'] : + self.parser.add_option ('-f','--file',action='store',dest='args_from_file',default=None, + help="read args from file") + + (self.options,self.args) = self.parser.parse_args() + + if mode=='show': + for arg in self.check_usual_args(): + self.vserver(arg).show(self.options.long_format) + elif mode=='stop': + for arg in self.check_usual_args(): + vs = self.vserver(arg) + vs.stop() + vs.show(True) + elif mode=='start': + for arg in self.check_usual_args(): + vs = self.vserver(arg) + vs.start() + vs.show(True) + elif mode=='check': 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 + for arg in self.check_usual_args(): + if not b2e(self.vserver(arg).check()): result=False return result elif mode=='rename': - [x,y]=args - return b2e(Vserver(x,options.dry_run).rename(y)) + [x,y]=self.args + return b2e(self.vserver(x).rename(y)) elif mode=='migrate': print 'migrate not yet implemented' return 1 @@ -371,4 +471,4 @@ class Main: return b2e(False) if __name__ == '__main__': - sys.exit( Main().run() ) + sys.exit( VserverTools().run() ) -- 2.43.0