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 ########################################
37 print >>sys.stderr, msg
41 ########################################
52 ########################################
54 def __init__(self,dry_run=False):
57 def header (self,message):
58 logger.log("==============="+message)
61 def run (self, argv, trash_stdout=False, trash_stderr=False, message="", force_dry_run=False):
63 print 'DRY_RUN',message+'RUNNING '+" ".join(argv)
66 if message: self.header(message),
67 if verbose: logger.log ('RUNNING '+" ".join(argv))
68 if trash_stdout and trash_stderr:
69 return subprocess.call(argv,stdout=file('/dev/null','w'),stderr=subprocess.STDOUT)
70 elif trash_stdout and not trash_stderr:
71 return subprocess.call(argv,stdout=file('/dev/null','w'))
72 elif not trash_stdout and trash_stderr:
73 return subprocess.call(argv,stderr=file('/dev/null','w'))
74 elif not trash_stdout and not trash_stderr:
75 return subprocess.call(argv)
77 # like shell's $(...) which used to be `...`
78 def backquote (self, argv, trash_stderr=False,message=""):
79 if self.dry_run or verbose:
80 if verbose: logger.log("WARNING: " + message + " backquote is actually running "+" ".join(argv))
82 return subprocess.Popen(argv,stdout=subprocess.PIPE).communicate()[0]
84 return subprocess.Popen(argv,stdout=subprocess.PIPE,stderr=file('/dev/null','w')).communicate()[0]
89 #################### the class
90 def __init__(self,hostname=None,key=None, username='root',dry_run=False):
91 self.hostname=hostname
92 if not hostname: self.hostname='localhost'
94 self.username=username
97 # inserts a backslash before each occurence of the following chars
98 # \ " ' < > & | ; ( ) $ * ~
100 def backslash_shell_specials (command):
103 if char in "\\\"'<>&|;()$*~":
109 # check main IP address against the provided hostname
111 def is_local_hostname (hostname):
112 if hostname == "localhost":
115 local_ip = socket.gethostbyname(socket.gethostname())
116 remote_ip = socket.gethostbyname(hostname)
117 return local_ip==remote_ip
119 logger.log("WARNING : something wrong in is_local_hostname with hostname=%s"%hostname)
123 def canonical_name (hostname):
125 ip=socket.gethostbyname(hostname)
126 return socket.gethostbyaddr(ip)[0]
128 logger.log( 'Warning - could not find canonical name for host %s'%hostname)
132 ########## building argv
134 return Ssh.is_local_hostname(self.hostname)
139 return ["-i",self.key]
141 def hostname_arg (self):
142 if not self.username:
145 return "%s@%s"%(self.username,self.hostname)
147 std_options="-o BatchMode=yes -o StrictHostKeyChecking=no -o CheckHostIP=no -o ConnectTimeout=5"
149 # command gets run on the right box
150 def argv (self, argv, keep_stdin=False):
154 if not keep_stdin: ssh_argv.append('-n')
155 ssh_argv += Ssh.std_options.split()
156 ssh_argv += self.key_argv()
157 ssh_argv.append (self.hostname_arg())
158 # using subprocess removes the need for special chars at this point
159 #ssh_argv += [Ssh.backslash_shell_specials(x) for x in argv]
164 def run(self, argv,*args, **kwds):
165 return Shell(self.dry_run).run(self.argv(argv),*args, **kwds)
166 # command is a string
167 def run_string(self, command, *args, **kwds):
168 return Shell(self.dry_run).run(self.argv(command.split()),*args, **kwds)
171 def backquote(self, argv, *args, **kwds):
172 return Shell(self.dry_run).backquote(self.argv(argv),*args, **kwds)
173 # command is a string
174 def backquote_string(self, command, *args, **kwds):
175 return Shell(self.dry_run).backquote(self.argv(command.split()),*args, **kwds)
177 ############################################################
180 def __init__ (self, name, dry_run=False):
182 (self.name,self.hostname)=name.split("@")
184 (self.name,self.hostname)=(name,'localhost')
186 self.ssh=Ssh(self.hostname,username='root',dry_run=self.dry_run)
187 self._is_running=None
190 def is_running (self):
191 if self._is_running is not None: return self._is_running
192 self._is_running = e2b(self.ssh.run_string('vserver %s status'%self.name,
193 trash_stdout=True,trash_stderr=True,
194 message='retrieving status for %s'%self.name))
195 def running_char (self):
196 if self._is_running is None: return '?'
197 elif not self._is_running: return '-'
202 self._is_running=None
203 return e2b(self.ssh.run_string("vserver %s stop"%self.name,message='Stopping %s'%self.name))
207 self._is_running=None
208 return e2b(self.ssh.run_string("vserver %s start"%self.name,message='Starting %s'%self.name))
210 def printable_simple(self): return self.name + '@' + self.hostname
211 def printable_dbg(self): return "%s@%s (%s)"%(self.name,self.hostname,self.running_char())
215 return "%s@%s (%s)"%(self.name,self.hostname,self.running_char())
218 return self.ssh.is_local()
221 def probe_host (hostname, dry_run=False):
222 canonical=Ssh.canonical_name(hostname)
223 ssh=Ssh(hostname,dry_run=dry_run)
224 ls=ssh.backquote_string("ls -d1 /vservers/*",trash_stderr=True)
225 return [ '%s@%s'%(path.replace('/vservers/',''),canonical) for path in ls.split()]
228 def rename (self, newname):
229 target=Vserver(newname)
230 if self.hostname != target.hostname:
231 # make user's like easier, no need to mention hostname again
232 if target.is_local():
233 target.hostname=self.hostname
235 logger.log('rename cannot rename (%s->%s) - must be on same box'%(self.hostname, target.hostname))
237 logger.log("Renaming %s into %s"%(self.printable(),target.printable_simple()))
242 xid=ssh.backquote(['cat','/etc/vservers/%(oldname)s/context'%locals()])
243 currently_running=self.is_running()
245 if currently_running:
246 result = result and self.stop()
248 and e2b(ssh.run_string("mv /vservers/%(oldname)s /vservers/%(newname)s"%locals())) \
249 and e2b(ssh.run_string("mv /etc/vservers/%(oldname)s /etc/vservers/%(newname)s"%locals())) \
250 and e2b(ssh.run_string("rm /etc/vservers/%(newname)s/run"%locals())) \
251 and e2b(ssh.run_string("ln -s /var/run/vservers/%(newname)s /etc/vservers/%(newname)s/run"%locals())) \
252 and e2b(ssh.run_string("rm /etc/vservers/%(newname)s/vdir"%locals())) \
253 and e2b(ssh.run_string("ln -s /etc/vservers/.defaults/vdirbase/%(newname)s /etc/vservers/%(newname)s/vdir"%locals())) \
254 and e2b(ssh.run_string("rm /etc/vservers/%(newname)s/cache"%locals())) \
255 and e2b(ssh.run_string("ln -s /etc/vservers/.defaults/cachebase/%(newname)s /etc/vservers/%(newname)s/cache"%locals())) \
256 and e2b(ssh.run_string("rm /var/run/vservers.rev/%(xid)s"%locals())) \
257 and e2b(ssh.run_string("ln -s /etc/vserver/%(newname)s /var/run/vservers.rev/%(xid)s"%locals()))
258 if currently_running:
259 result = result and self.start()
262 def ok(self,message): logger.log('OK %s: %s'%(self.printable(),message))
263 def error(self,message): logger.log('ERROR %s: %s'%(self.printable(),message))
264 def warning(self,message): logger.log( 'WARNING %s: %s'%(self.printable(),message))
270 hostname=ssh.backquote_string("cat /etc/vservers/%(name)s/uts/nodename"%locals()).strip()
272 self.error('no uts/nodename')
274 ip=ssh.backquote_string("cat /etc/vservers/%(name)s/interfaces/0/ip"%locals()).strip()
276 self.error('no interfaces/0/ip')
279 expected_ip=socket.gethostbyname(hostname)
280 if expected_ip == ip:
281 self.ok('hostname %s maps to IP %s'%(hostname,ip))
283 self.error('IP mismatch, current %s -- expected %s'%(ip,expected_ip))
286 self.warning ('could not resolve hostname <%s>'%hostname)
287 expected_ip='255.255.255.255'
289 mask_path="/etc/vservers/%(name)s/interfaces/mask"%locals()
290 mask_exists = e2b(ssh.run_string('ls %s'%mask_path,trash_stderr=True))
292 self.error('found a mask file in %s'%(self.printable(),mask_path))
295 self.ok('no mask file %s'%mask_path)
301 # expected arg numbers can be either
302 # negative int, e.g. -1 means at least one arg
303 # positive int, exactly that number of args
304 # list of ints : allowed number of args
305 'probe' : "hostname(s)\n\tprobes all hostnames and returns the found vservers",
306 'netcheck' : "vsname\n\tchecks the network config for a vserver",
307 'rename' : "oldname newname\n\trename a vserver - both named must be local",
308 'migrate' : "oldname hostname@newname\n\tmigrate a vserver over to a new box; must be run on the current box\n\tnot implemented yet",
311 def read_args_from_file (self,args_from_file):
312 if not args_from_file: return []
314 raw=file(args_from_file).read().split()
315 return [x for x in raw if x.find('#')<0]
317 logger.log('ERROR: Could not open args file %s'%args_from_file)
323 for function in Main.modes.keys():
324 if sys.argv[0].find(function) >= 0:
326 mode_desc = Main.modes[mode]
329 print "Unsupported command",sys.argv[0]
330 print "Supported commands:" + " ".join(Main.modes.keys())
333 parser=OptionParser(usage="%prog " + mode_desc)
334 parser.add_option('-n','--dry-run',dest='dry_run',action='store_true',default=False,
336 if mode == 'probe' or mode == 'netcheck':
337 parser.add_option ('-f','--file',action='store',dest='args_from_file',default=None,
338 help='read args from file')
340 (options,args) = parser.parse_args()
341 parser.set_usage("%prog " + mode_desc)
343 # check number of arguments
345 parser.print_help(msg)
351 if not options.args_from_file and len(args)==0: bail_out('no arg specified')
352 for arg in args + self.read_args_from_file(options.args_from_file):
353 names += Vserver.probe_host(arg,options.dry_run)
355 for name in names: print name
357 elif mode=='netcheck':
359 if not options.args_from_file and len(args)==0: bail_out('no arg specified')
360 for arg in args + self.read_args_from_file(options.args_from_file):
361 if not b2e(Vserver(arg,options.dry_run).netcheck()): result=False
365 return b2e(Vserver(x,options.dry_run).rename(y))
366 elif mode=='migrate':
367 print 'migrate not yet implemented'
370 print 'unknown mode %s'%mode
373 if __name__ == '__main__':
374 sys.exit( Main().run() )