#!/usr/bin/python
#
-# $Id$
-# $URL$
import sys
import os.path
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()
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:
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
# \ " ' < > & | ; ( ) $ * ~
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
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):
+ self.dry_run=dry_run
+ self.verbose=verbose
+ self._running_state=None
+ self._xid=None
+ self._autostart=None
try:
- (self.name,self.hostname)=name.split("@")
+ (self.name,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.name,hostname)=(name,'localhost')
+ self.set_hostname(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
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
self.ok('no mask file %s'%mask_path)
return result
-class Main:
+ def delete (self):
+ # ommitting --silent just silently returns - stdin must be closed or something
+ command=["vserver","--silent",self.name,"delete"]
+ return e2b (self.ssh.run (command))
+
+ # 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/<>"))
+ result = result and e2b(ssh.run("mv /etc/vservers/%(oldname)s /etc/vservers/%(newname)s"%locals(),
+ message="tweaking /etc/vservers/<>"))
+ result = result and e2b(ssh.run("mv /vservers/.pkg/%(oldname)s /vservers/.pkg/%(newname)s"%locals(),
+ message="renaming /vservers/.pkg/<>"))
+ result = result and e2b(ssh.run("rm /etc/vservers/%(newname)s/run"%locals(),
+ message="cleaning /etc/vservers/<>/run"))
+ result = result and e2b(ssh.run("ln -s /var/run/vservers/%(newname)s /etc/vservers/%(newname)s/run"%locals(),
+ message="adjusting /etc/vservers/<>/run"))
+ result = result and e2b(ssh.run("rm /etc/vservers/%(newname)s/vdir"%locals(),
+ message="cleaning /etc/vservers/<>/vdir"))
+ result = result and e2b(ssh.run("ln -s /etc/vservers/.defaults/vdirbase/%(newname)s /etc/vservers/%(newname)s/vdir"%locals(),
+ message="adjusting /etc/vservers/<>/vdir"))
+ result = result and e2b(ssh.run("rm /etc/vservers/%(newname)s/cache"%locals(),
+ message="cleaning /etc/vservers/<>/cache"))
+ result = result and e2b(ssh.run("ln -s /etc/vservers/.defaults/cachebase/%(newname)s /etc/vservers/%(newname)s/cache"%locals(),
+ message="adjusting /etc/vservers/<>/cache"))
+ result = result and e2b(ssh.run("rm /var/run/vservers.rev/%(xid)s"%locals(),
+ message="cleaning /var/run/vservers.rev/<>"))
+ result = result and e2b(ssh.run("ln -s /etc/vservers/%(newname)s /var/run/vservers.rev/%(xid)s"%locals(),
+ message="adjusting /var/run/vservers.rev/<>"))
+ result = result and e2b(ssh.run("sed -i -e s,%(oldname)s,%(newname)s,g /etc/vservers/%(newname)s/name"%locals(),
+ message="adjusting /etc/vservers/<>/name"))
+ result = result and e2b(ssh.run("sed -i -e s,%(oldname)s,%(newname)s,g /etc/vservers/%(newname)s/uts/nodename"%locals(),
+ message="adjusting /etc/vservers/<>/uts/nodename"))
+ # ignore rsult
+ e2b(ssh.run("sed -i -e s,\\[%(oldname)s\\],\\[%(newname)s\\], /vservers/%(newname)s/root/.profile"%locals(),
+ message="adjusting /vservers/<>/root/.profile"))
+
+# # 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\tsanity check - ip from hostname, netmask, ...",
+ 'delete' : "vsname(s)\n\tdelete vservers",
'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
+ for arg in self.check_usual_args():
+ if not b2e(self.vserver(arg).check()): result=False
+ return result
+ elif mode == 'delete':
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).delete()): result=False
+ self.vserver(arg).show(True)
return result
- elif mode=='rename':
- [x,y]=args
- return b2e(Vserver(x,options.dry_run).rename(y))
- elif mode=='migrate':
+ elif mode == 'rename':
+ [x,y]=self.args
+ return b2e(self.vserver(x).rename(y))
+ elif mode == 'migrate':
print 'migrate not yet implemented'
return 1
else:
return b2e(False)
if __name__ == '__main__':
- sys.exit( Main().run() )
+ sys.exit( VserverTools().run() )