10 from optparse import OptionParser
14 # Thierry Parmentelat - INRIA
16 # class for issuing commands on a box, either local or remote
18 # the notion of 'buildname' is for providing each test run with a dir of its own
19 # buildname is generally the name of the build being tested, and can be considered unique
21 # thus 'run_in_buildname' mostly :
22 # (*) either runs locally in . - as on a local node we are already in a dedicated directory
23 # (*) or makes sure that there's a remote dir called 'buildname' and runs in it
25 # also, the copy operations
26 # (*) either do nothing if ran locally
27 # (*) or copy a local file into the remote 'buildname'
30 ########################################
36 def log (self,msg,level=0):
37 if level>0: print >> sys.stderr,level*4*'=',
38 print >>sys.stderr, msg
42 ########################################
51 ########################################
53 def __init__(self,dry_run=False,verbose=False):
57 def header (self,message):
58 logger.log(message,level=1)
60 # convenience : if argv is a string, we just split it to get a list
61 # if args may contain spaces, it's safer to build the list yourself
62 def normalize (self, argv):
63 if isinstance (argv,list): return argv
64 if isinstance (argv,str): return argv.split()
65 else: raise Exception, "Unsupported command type in Shell - %r"%argv
68 # in general dry_run ... does not run
69 # there are cases where the command is harmless and required for the rest to proceed correctly
70 # in this case you can set force_dry_run
71 def run (self, argv, trash_stdout=False, trash_stderr=False, message="", force_dry_run=False):
72 argv=self.normalize(argv)
73 if self.verbose and message: self.header(message),
74 if self.dry_run and not force_dry_run:
75 print 'WOULD RUN '+" ".join(argv)
79 if force_dry_run and self.verbose: helper += ' (forced)'
80 if self.verbose: logger.log (helper+' '+" ".join(argv))
81 if trash_stdout and trash_stderr:
82 return subprocess.call(argv,stdout=file('/dev/null','w'),stderr=subprocess.STDOUT)
83 elif trash_stdout and not trash_stderr:
84 return subprocess.call(argv,stdout=file('/dev/null','w'))
85 elif not trash_stdout and trash_stderr:
86 return subprocess.call(argv,stderr=file('/dev/null','w'))
87 elif not trash_stdout and not trash_stderr:
88 return subprocess.call(argv)
90 # like shell's $(...) which used to be `...` - cannot trash stdout of course
91 # ditto for the dry_run mode
92 def backquote (self, argv, trash_stderr=False,message="", force_dry_run=False):
93 argv=self.normalize (argv)
94 if self.verbose and message: self.header(message),
95 if self.dry_run and not force_dry_run:
96 print 'WOULD RUN $('+" ".join(argv) + ')'
99 return subprocess.Popen(argv,stdout=subprocess.PIPE).communicate()[0]
101 return subprocess.Popen(argv,stdout=subprocess.PIPE,stderr=file('/dev/null','w')).communicate()[0]
106 #################### the class
107 def __init__(self,hostname=None,key=None, username='root',dry_run=False,verbose=False):
108 self.hostname=hostname
109 if not hostname: self.hostname='localhost'
111 self.username=username
112 Shell.__init__(self,dry_run=dry_run,verbose=verbose)
114 # inserts a backslash before each occurence of the following chars
115 # \ " ' < > & | ; ( ) $ * ~
117 def backslash_shell_specials (command):
120 if char in "\\\"'<>&|;()$*~":
126 # check main IP address against the provided hostname
128 def is_local_hostname (hostname):
129 if hostname == "localhost":
132 local_ip = socket.gethostbyname(socket.gethostname())
133 remote_ip = socket.gethostbyname(hostname)
134 return local_ip==remote_ip
136 logger.log("WARNING : something wrong in is_local_hostname with hostname=%s"%hostname)
140 def canonical_name (hostname):
142 ip=socket.gethostbyname(hostname)
143 return socket.gethostbyaddr(ip)[0]
145 logger.log( "WARNING - could not find canonical name for host %s"%hostname)
149 ########## building argv
151 return Ssh.is_local_hostname(self.hostname)
156 return ["-i",self.key]
158 def hostname_arg (self):
159 if not self.username:
162 return "%s@%s"%(self.username,self.hostname)
164 std_options="-o BatchMode=yes -o StrictHostKeyChecking=no -o CheckHostIP=no -o ConnectTimeout=5"
166 # command gets run on the right box
167 def argv (self, argv, keep_stdin=False):
171 if not keep_stdin: ssh_argv.append('-n')
172 ssh_argv += Ssh.std_options.split()
173 ssh_argv += self.key_argv()
174 ssh_argv.append (self.hostname_arg())
175 # using subprocess removes the need for special chars at this point
176 #ssh_argv += [Ssh.backslash_shell_specials(x) for x in argv]
180 def run(self, argv,*args, **kwds):
181 return Shell.run(self,self.argv(self.normalize(argv)),*args, **kwds)
182 def backquote(self, argv, *args, **kwds):
183 return Shell.backquote(self,self.argv(self.normalize(argv)),*args, **kwds)
185 ############################################################
188 def __init__ (self, name, dry_run=False,verbose=False):
191 self._running_state=None
195 (self.name,hostname)=name.split("@")
197 (self.name,hostname)=(name,'localhost')
198 self.set_hostname(hostname)
200 def set_hostname (self, hostname):
201 self.hostname=hostname
202 self.ssh=Ssh(self.hostname,username='root',dry_run=self.dry_run,verbose=self.verbose)
204 def ok(self,message):
205 if self.verbose: logger.log('OK %s: %s'%(self.printable(),message))
206 def warning(self,message):
207 if self.verbose: logger.log( 'WARNING %s: %s'%(self.printable(),message))
208 def error(self,message):
209 logger.log('ERROR %s: %s'%(self.printable(),message))
212 def running_state (self):
213 if self._running_state is not None: return self._running_state
214 status_retcod= self.ssh.run('vserver %s status'%self.name,
215 trash_stdout=True,trash_stderr=True,
216 message='retrieving status for %s'%self.name,
218 if status_retcod == 0: self._running_state = 'start'
219 elif status_retcod == 3: self._running_state = 'stop'
220 else: self._running_state = 'unknown'
221 return self._running_state
223 def running_char (self):
224 if self._running_state is None: return '?'
225 elif self._running_state == 'unknown' : return '!'
226 elif self._running_state == 'stop': return '-'
231 if self._xid is not None: return self._xid
232 self._xid = self.ssh.backquote(['cat','/etc/vservers/%s/context'%self.name],
233 force_dry_run=True,trash_stderr=True,
234 message='Retrieving xid for %s'%self.name).strip()
237 def autostart (self):
238 if self._autostart is not None: return self._autostart
239 self._autostart = self.ssh.backquote(['cat','/etc/vservers/%s/apps/init/mark'%self.name],
240 force_dry_run=True,trash_stderr=True,
241 message='Retrieving autostart status for %s'%self.name).strip()=='default'
242 return self._autostart
246 self._running_state=None
247 return e2b(self.ssh.run("vserver %s stop"%self.name,
248 message='Stopping %s'%self.name))
252 self._running_state=None
253 return e2b(self.ssh.run("vserver %s start"%self.name,
254 message='Starting %s'%self.name))
256 def printable_simple(self): return self.name + '@' + self.hostname
257 def printable_dbg(self):
259 if not self.autostart(): autostart_part='< >'
260 return autostart_part+" (%s) [%s] %s@%s"%(self.running_char(),self.xid(),self.name,self.hostname)
262 # force fetch cache info
266 return self.printable_dbg()
269 return self.ssh.is_local()
271 # does it have a pattern
272 def has_pattern (self):
273 return self.name.find('*') >= 0
276 def probe_pattern (self):
278 ls=self.ssh.backquote("ls -d1 /vservers/%s"%pattern,
279 trash_stderr=True,force_dry_run=True,
280 message='scanning /vservers with pattern %s on %s'%(pattern,self.hostname))
281 return [ '%s@%s'%(path.replace('/vservers/',''),self.hostname) for path in ls.split()]
283 #################### toplevel methods for main
284 def show (self, long_format):
286 print self.printable()
288 print self.printable_simple()
294 hostname=ssh.backquote("cat /etc/vservers/%(name)s/uts/nodename"%locals()).strip()
296 self.warning('no uts/nodename')
297 ip=ssh.backquote("cat /etc/vservers/%(name)s/interfaces/0/ip"%locals()).strip()
299 self.error('no interfaces/0/ip')
302 expected_ip=socket.gethostbyname(hostname)
303 if expected_ip == ip:
304 self.ok('hostname %s maps to IP %s'%(hostname,ip))
306 self.error('IP mismatch, current %s -- expected %s'%(ip,expected_ip))
309 self.warning ('could not resolve hostname <%s>'%hostname)
310 expected_ip='255.255.255.255'
312 mask_path="/etc/vservers/%(name)s/interfaces/mask"%locals()
313 mask_exists = e2b(ssh.run('ls %s'%mask_path,trash_stderr=True))
315 self.error('found a mask file in %s'%(self.printable(),mask_path))
318 self.ok('no mask file %s'%mask_path)
322 # ommitting --silent just silently returns - stdin must be closed or something
323 command=["vserver","--silent",self.name,"delete"]
324 return e2b (self.ssh.run (command))
327 def rename (self, newname):
328 target=Vserver(newname)
329 if self.hostname != target.hostname:
330 # make user's like easier, no need to mention hostname again
331 if target.is_local():
332 target.hostname=self.hostname
334 logger.log('ERROR:rename cannot rename (%s->%s) - must be on same box'%(self.hostname, target.hostname))
336 logger.log("%s into %s"%(self.printable(),target.printable_simple()),level=2)
342 currently_running=self.running_state()=='start'
344 if currently_running:
345 result = result and self.stop()
347 and e2b(ssh.run("mv /vservers/%(oldname)s /vservers/%(newname)s"%locals(),
348 message="tweaking /vservers/<>")) \
349 and e2b(ssh.run("mv /etc/vservers/%(oldname)s /etc/vservers/%(newname)s"%locals(),
350 message="tweaking /etc/vservers/<>")) \
351 and e2b(ssh.run("rm /etc/vservers/%(newname)s/run"%locals(),
352 message="cleaning /etc/vservers/<>/run")) \
353 and e2b(ssh.run("ln -s /var/run/vservers/%(newname)s /etc/vservers/%(newname)s/run"%locals(),
354 message="adjusting /etc/vservers/<>/run")) \
355 and e2b(ssh.run("rm /etc/vservers/%(newname)s/vdir"%locals(),
356 message="cleaning /etc/vservers/<>/vdir")) \
357 and e2b(ssh.run("ln -s /etc/vservers/.defaults/vdirbase/%(newname)s /etc/vservers/%(newname)s/vdir"%locals(),
358 message="adjusting /etc/vservers/<>/vdir")) \
359 and e2b(ssh.run("rm /etc/vservers/%(newname)s/cache"%locals(),
360 message="cleaning /etc/vservers/<>/cache")) \
361 and e2b(ssh.run("ln -s /etc/vservers/.defaults/cachebase/%(newname)s /etc/vservers/%(newname)s/cache"%locals(),
362 message="adjusting /etc/vservers/<>/cache")) \
363 and e2b(ssh.run("rm /var/run/vservers.rev/%(xid)s"%locals(),
364 message="cleaning /var/run/vservers.rev/<>")) \
365 and e2b(ssh.run("ln -s /etc/vserver/%(newname)s /var/run/vservers.rev/%(xid)s"%locals(),
366 message="adjusting /var/run/vservers.rev/<>"))
367 # # refreshing target instance
368 target=Vserver(newname,dry_run=self.dry_run,verbose=self.verbose)
369 target.set_hostname(self.hostname)
370 if currently_running and not self.dry_run:
371 result = result and target.start()
372 print target.printable()
378 'show' : "vsname(s)\n\tdisplay status & details",
379 'start': "vsname(s)\n\tstart vservers",
380 'stop' : "vsname(s)\n\tstop vservers",
381 'check' : "vsname(s)\n\tsanity check - ip from hostname, netmask, ...",
382 'delete' : "vsname(s)\n\tdelete vservers",
383 'rename' : "oldname newname\n\trename a vserver - both named must be local",
384 'migrate' : "oldname hostname@newname\n\tmigrate a vserver over to a new box\n\tnot implemented yet",
388 self.parser=OptionParser()
392 def vserver(self, vsname):
393 return Vserver(vsname,dry_run=self.options.dry_run,verbose=self.options.verbose)
395 def probe_star_arg (self, arg):
397 if vs.has_pattern(): return vs.probe_pattern()
400 def read_args_from_file (self):
401 if not self.options.args_from_file: return []
404 raw_args=file(self.options.args_from_file).read().split()
406 result += self.probe_star_arg(arg)
409 logger.log('ERROR: Could not open args file %s'%self.options.args_from_file)
412 def check_usual_args(self):
413 if not self.options.args_from_file and len(self.args)==0:
414 print ('no arg specified')
415 self.parser.print_help()
418 for arg in self.args: result += self.probe_star_arg(arg)
419 result += self.read_args_from_file()
427 for function in VserverTools.modes.keys():
428 if sys.argv[0].find(function) >= 0:
430 mode_desc = VserverTools.modes[mode]
433 print "Unsupported command",sys.argv[0]
434 print "Supported commands:" + " ".join(VserverTools.modes.keys())
437 self.parser.set_usage("%prog " + mode_desc)
438 self.parser.add_option('-n','--dry-run',dest='dry_run',action='store_true',default=False,
440 self.parser.add_option('-v','--verbose',dest='verbose',action='store_true',default=False,
442 self.parser.add_option ('-l','--long',action='store_true', dest='long_format',default=False,
443 help="show more detailed result")
445 if mode not in ['rename','migrate'] :
446 self.parser.add_option ('-f','--file',action='store',dest='args_from_file',default=None,
447 help="read args from file")
449 (self.options,self.args) = self.parser.parse_args()
452 for arg in self.check_usual_args():
453 self.vserver(arg).show(self.options.long_format)
455 for arg in self.check_usual_args():
456 vs = self.vserver(arg)
459 elif mode == 'start':
460 for arg in self.check_usual_args():
461 vs = self.vserver(arg)
464 elif mode == 'check':
466 for arg in self.check_usual_args():
467 if not b2e(self.vserver(arg).check()): result=False
469 elif mode == 'delete':
471 for arg in self.check_usual_args():
472 if not b2e(self.vserver(arg).delete()): result=False
473 self.vserver(arg).show(True)
475 elif mode == 'rename':
477 return b2e(self.vserver(x).rename(y))
478 elif mode == 'migrate':
479 print 'migrate not yet implemented'
482 print 'unknown mode %s'%mode
485 if __name__ == '__main__':
486 sys.exit( VserverTools().run() )