f37 -> f39
[infrastructure.git] / scripts / vserver_tools.py
1 #!/usr/bin/python
2 #
3
4 import sys
5 import os.path
6 import pdb
7 import subprocess
8 from optparse import OptionParser
9 import socket
10
11 #
12 # Thierry Parmentelat - INRIA
13 #
14 # class for issuing commands on a box, either local or remote
15 #
16 # the notion of 'buildname' is for providing each test run with a dir of its own
17 # buildname is generally the name of the build being tested, and can be considered unique
18 #
19 # thus 'run_in_buildname' mostly :
20 # (*) either runs locally in . - as on a local node we are already in a dedicated directory
21 # (*) or makes sure that there's a remote dir called 'buildname' and runs in it
22 #
23 # also, the copy operations
24 # (*) either do nothing if ran locally
25 # (*) or copy a local file into the remote 'buildname' 
26
27
28 ########################################
29 class Logger:
30
31     def __init__(self):
32         pass
33
34     def log (self,msg,level=0):
35         if level>0: print >> sys.stderr,level*4*'=',
36         print >>sys.stderr, msg
37
38 logger=Logger()
39
40 ########################################
41 # bool_to_exit_code
42 def b2e (b):
43     if b: return 0
44     else: return 1
45 # exit_to_bool_code
46 def e2b (b):
47     return b==0
48
49 ########################################
50 class Shell:
51     def __init__(self,dry_run=False,verbose=False):
52         self.dry_run=dry_run
53         self.verbose=verbose
54
55     def header (self,message):
56         logger.log(message,level=1)
57
58     # convenience : if argv is a string, we just split it to get a list
59     # if args may contain spaces, it's safer to build the list yourself
60     def normalize (self, argv):
61         if isinstance (argv,list): return argv
62         if isinstance (argv,str): return argv.split()
63         else: raise Exception, "Unsupported command type in Shell - %r"%argv
64
65     # return an exit code
66     # in general dry_run ... does not run
67     # there are cases where the command is harmless and required for the rest to proceed correctly
68     # in this case you can set force_dry_run
69     def run (self, argv, trash_stdout=False, trash_stderr=False, message="", force_dry_run=False):
70         argv=self.normalize(argv)
71         if self.verbose and message: self.header(message),
72         if self.dry_run and not force_dry_run:
73             print 'WOULD RUN '+" ".join(argv)
74             return 0
75         else:
76             helper='RUNNING'
77             if force_dry_run and self.verbose: helper += ' (forced)'
78             if self.verbose: logger.log (helper+' '+" ".join(argv))
79             if trash_stdout and trash_stderr:
80                 return subprocess.call(argv,stdout=file('/dev/null','w'),stderr=subprocess.STDOUT)
81             elif trash_stdout and not trash_stderr:
82                 return subprocess.call(argv,stdout=file('/dev/null','w'))
83             elif not trash_stdout and trash_stderr:
84                 return subprocess.call(argv,stderr=file('/dev/null','w'))
85             elif not trash_stdout and not trash_stderr:
86                 return subprocess.call(argv)
87             
88     # like shell's $(...) which used to be `...` - cannot trash stdout of course
89     # ditto for the dry_run mode
90     def backquote (self, argv, trash_stderr=False,message="", force_dry_run=False):
91         argv=self.normalize (argv)
92         if self.verbose and message: self.header(message),
93         if self.dry_run and not force_dry_run:
94             print 'WOULD RUN $('+" ".join(argv) + ')'
95             return ""
96         if not trash_stderr:
97             return subprocess.Popen(argv,stdout=subprocess.PIPE).communicate()[0]
98         else:
99             return subprocess.Popen(argv,stdout=subprocess.PIPE,stderr=file('/dev/null','w')).communicate()[0]
100             
101 ####################
102 class Ssh (Shell):
103     
104     #################### the class
105     def __init__(self,hostname=None,key=None, username='root',dry_run=False,verbose=False):
106         self.hostname=hostname
107         if not hostname: self.hostname='localhost'
108         self.key=key
109         self.username=username
110         Shell.__init__(self,dry_run=dry_run,verbose=verbose)
111
112     # inserts a backslash before each occurence of the following chars
113     # \ " ' < > & | ; ( ) $ * ~ 
114     @staticmethod
115     def backslash_shell_specials (command):
116         result=''
117         for char in command:
118             if char in "\\\"'<>&|;()$*~":
119                 result +='\\'+char
120             else:
121                 result +=char
122         return result
123
124     # check main IP address against the provided hostname
125     @staticmethod
126     def is_local_hostname (hostname):
127         if hostname == "localhost":
128             return True
129         try:
130             local_ip = socket.gethostbyname(socket.gethostname())
131             remote_ip = socket.gethostbyname(hostname)
132             return local_ip==remote_ip
133         except:
134             logger.log("WARNING : something wrong in is_local_hostname with hostname=%s"%hostname)
135             return False
136
137     @staticmethod
138     def canonical_name (hostname):
139         try:
140             ip=socket.gethostbyname(hostname)
141             return socket.gethostbyaddr(ip)[0]
142         except:
143             logger.log( "WARNING - could not find canonical name for host %s"%hostname)
144             return hostname
145
146
147     ########## building argv
148     def is_local(self):
149         return Ssh.is_local_hostname(self.hostname)
150      
151     def key_argv (self):
152         if not self.key:
153             return []
154         return ["-i",self.key]
155
156     def hostname_arg (self):
157         if not self.username:
158             return self.hostname
159         else:
160             return "%s@%s"%(self.username,self.hostname)
161     
162     std_options="-o BatchMode=yes -o StrictHostKeyChecking=no -o CheckHostIP=no -o ConnectTimeout=5"
163     
164     # command gets run on the right box
165     def argv (self, argv, keep_stdin=False):
166         if self.is_local():
167             return argv
168         ssh_argv = ["ssh"]
169         if not keep_stdin: ssh_argv.append('-n')
170         ssh_argv += Ssh.std_options.split()
171         ssh_argv += self.key_argv()
172         ssh_argv.append (self.hostname_arg())
173         # using subprocess removes the need for special chars at this point
174         #ssh_argv += [Ssh.backslash_shell_specials(x) for x in argv]
175         ssh_argv += argv
176         return ssh_argv
177
178     def run(self, argv,*args, **kwds):
179         return Shell.run(self,self.argv(self.normalize(argv)),*args, **kwds)
180     def backquote(self, argv, *args, **kwds):
181         return Shell.backquote(self,self.argv(self.normalize(argv)),*args, **kwds)
182
183 ############################################################
184 class Vserver:
185
186     def __init__ (self, name, dry_run=False,verbose=False):
187         self.dry_run=dry_run
188         self.verbose=verbose
189         self._running_state=None
190         self._xid=None
191         self._autostart=None
192         try:
193             (self.name,hostname)=name.split("@")
194         except:
195             (self.name,hostname)=(name,'localhost')
196         self.set_hostname(hostname)
197
198     def set_hostname (self, hostname):
199         self.hostname=hostname
200         self.ssh=Ssh(self.hostname,username='root',dry_run=self.dry_run,verbose=self.verbose)
201
202     def ok(self,message): 
203         if self.verbose: logger.log('OK %s: %s'%(self.printable(),message))
204     def warning(self,message): 
205         if self.verbose: logger.log( 'WARNING %s: %s'%(self.printable(),message))
206     def error(self,message): 
207         logger.log('ERROR %s: %s'%(self.printable(),message))
208
209     # cache run mode
210     def running_state (self):
211         if self._running_state is not None: return self._running_state
212         status_retcod= self.ssh.run('vserver %s status'%self.name,
213                                     trash_stdout=True,trash_stderr=True,
214                                     message='retrieving status for %s'%self.name,
215                                     force_dry_run=True)
216         if status_retcod == 0:          self._running_state = 'start'
217         elif status_retcod == 3:        self._running_state = 'stop'
218         else:                           self._running_state = 'unknown'
219         return self._running_state
220
221     def running_char (self):
222         if self._running_state is None:         return '?'
223         elif self._running_state == 'unknown' : return '!'
224         elif self._running_state == 'stop':     return '-'
225         else:                                   return '+'
226
227     # cache xid
228     def xid (self):
229         if self._xid is not None: return self._xid
230         self._xid = self.ssh.backquote(['cat','/etc/vservers/%s/context'%self.name],
231                                        force_dry_run=True,trash_stderr=True,
232                                        message='Retrieving xid for %s'%self.name).strip()
233         return self._xid
234
235     def autostart (self):
236         if self._autostart is not None: return self._autostart
237         self._autostart = self.ssh.backquote(['cat','/etc/vservers/%s/apps/init/mark'%self.name],
238                                              force_dry_run=True,trash_stderr=True,
239                                              message='Retrieving autostart status for %s'%self.name).strip()=='default'
240         return self._autostart
241
242     def stop (self):
243         # uncache
244         self._running_state=None
245         return e2b(self.ssh.run("vserver %s stop"%self.name,
246                                 message='Stopping %s'%self.name))
247
248     def start (self):
249         # uncache
250         self._running_state=None
251         return e2b(self.ssh.run("vserver %s start"%self.name,
252                                 message='Starting %s'%self.name))
253
254     def printable_simple(self): return self.name + '@' + self.hostname
255     def printable_dbg(self): 
256         autostart_part='<*>'
257         if not self.autostart(): autostart_part='< >'
258         return autostart_part+" (%s) [%s] %s@%s"%(self.running_char(),self.xid(),self.name,self.hostname)
259     def printable(self): 
260         # force fetch cache info
261         self.running_state()
262         self.xid()
263         self.autostart()
264         return self.printable_dbg()
265
266     def is_local (self):
267         return self.ssh.is_local()
268
269     # does it have a pattern
270     def has_pattern (self):
271         return self.name.find('*') >= 0
272
273     # probe it
274     def probe_pattern (self):
275         pattern=self.name
276         ls=self.ssh.backquote("ls -d1 /vservers/%s"%pattern,
277                               trash_stderr=True,force_dry_run=True,
278                               message='scanning /vservers with pattern %s on %s'%(pattern,self.hostname))
279         return [ '%s@%s'%(path.replace('/vservers/',''),self.hostname) for path in ls.split()]
280
281     #################### toplevel methods for main
282     def show (self, long_format):
283         if long_format:
284             print self.printable()
285         else:
286             print self.printable_simple()
287
288     def check(self):
289         result=True
290         ssh=self.ssh
291         name=self.name
292         hostname=ssh.backquote("cat /etc/vservers/%(name)s/uts/nodename"%locals()).strip()
293         if not hostname: 
294             self.warning('no uts/nodename')
295         ip=ssh.backquote("cat /etc/vservers/%(name)s/interfaces/0/ip"%locals()).strip()
296         if not ip: 
297             self.error('no interfaces/0/ip')
298             result=False
299         try:
300             expected_ip=socket.gethostbyname(hostname)
301             if expected_ip == ip:
302                 self.ok('hostname %s maps to IP %s'%(hostname,ip))
303             else:
304                 self.error('IP mismatch, current %s -- expected %s'%(ip,expected_ip))
305                 result=False
306         except:
307             self.warning ('could not resolve hostname <%s>'%hostname)
308             expected_ip='255.255.255.255'
309             result=False
310         mask_path="/etc/vservers/%(name)s/interfaces/mask"%locals()
311         mask_exists = e2b(ssh.run('ls %s'%mask_path,trash_stderr=True))
312         if mask_exists:
313             self.error('found a mask file in %s'%(self.printable(),mask_path))
314             result=False
315         else:
316             self.ok('no mask file %s'%mask_path)
317         return result
318         
319     def delete (self):
320         # ommitting --silent just silently returns - stdin must be closed or something
321         command=["vserver","--silent",self.name,"delete"]
322         return e2b (self.ssh.run (command))
323
324     # renaming
325     def rename (self, newname):
326         target=Vserver(newname)
327         if self.hostname != target.hostname:
328             # make user's like easier, no need to mention hostname again
329             if target.is_local():
330                 target.hostname=self.hostname
331             else:
332                 logger.log('ERROR:rename cannot rename (%s->%s) - must be on same box'%(self.hostname, target.hostname))
333                 return False
334         logger.log("%s into %s"%(self.printable(),target.printable_simple()),level=2)
335         
336         ssh=self.ssh
337         oldname=self.name
338         newname=target.name
339         xid=self.xid()
340         currently_running=self.running_state()=='start'
341         result=True
342         if currently_running:
343             result = result and self.stop()
344         result = result and e2b(ssh.run("mv /vservers/%(oldname)s /vservers/%(newname)s"%locals(),
345                                         message="tweaking /vservers/<>"))
346         result = result and e2b(ssh.run("mv /etc/vservers/%(oldname)s /etc/vservers/%(newname)s"%locals(),
347                                         message="tweaking /etc/vservers/<>")) 
348         result = result and e2b(ssh.run("mv /vservers/.pkg/%(oldname)s /vservers/.pkg/%(newname)s"%locals(),
349                                         message="renaming /vservers/.pkg/<>")) 
350         result = result and e2b(ssh.run("rm /etc/vservers/%(newname)s/run"%locals(),
351                                         message="cleaning /etc/vservers/<>/run")) 
352         result = result and e2b(ssh.run("ln -s /var/run/vservers/%(newname)s /etc/vservers/%(newname)s/run"%locals(),
353                                         message="adjusting /etc/vservers/<>/run")) 
354         result = result and e2b(ssh.run("rm /etc/vservers/%(newname)s/vdir"%locals(),
355                                         message="cleaning /etc/vservers/<>/vdir")) 
356         result = result and e2b(ssh.run("ln -s /etc/vservers/.defaults/vdirbase/%(newname)s /etc/vservers/%(newname)s/vdir"%locals(),
357                                         message="adjusting /etc/vservers/<>/vdir")) 
358         result = result and e2b(ssh.run("rm /etc/vservers/%(newname)s/cache"%locals(),
359                                         message="cleaning /etc/vservers/<>/cache")) 
360         result = result and e2b(ssh.run("ln -s /etc/vservers/.defaults/cachebase/%(newname)s /etc/vservers/%(newname)s/cache"%locals(),
361                                         message="adjusting /etc/vservers/<>/cache")) 
362         result = result and e2b(ssh.run("rm /var/run/vservers.rev/%(xid)s"%locals(),
363                                         message="cleaning /var/run/vservers.rev/<>")) 
364         result = result and e2b(ssh.run("ln -s /etc/vservers/%(newname)s /var/run/vservers.rev/%(xid)s"%locals(),
365                                         message="adjusting /var/run/vservers.rev/<>")) 
366         result = result and e2b(ssh.run("sed -i -e s,%(oldname)s,%(newname)s,g /etc/vservers/%(newname)s/name"%locals(),
367                                         message="adjusting /etc/vservers/<>/name"))
368         result = result and e2b(ssh.run("sed -i -e s,%(oldname)s,%(newname)s,g /etc/vservers/%(newname)s/uts/nodename"%locals(),
369                                         message="adjusting /etc/vservers/<>/uts/nodename"))
370         # ignore rsult
371         e2b(ssh.run("sed -i -e s,\\[%(oldname)s\\],\\[%(newname)s\\], /vservers/%(newname)s/root/.profile"%locals(),
372                                         message="adjusting /vservers/<>/root/.profile"))
373
374 #        # refreshing target instance
375         target=Vserver(newname,dry_run=self.dry_run,verbose=self.verbose)
376         target.set_hostname(self.hostname)
377         if currently_running and not self.dry_run: 
378             result = result and target.start()
379         print target.printable()
380         return result
381             
382 class VserverTools:
383
384     modes={ 
385         'show' : "vsname(s)\n\tdisplay status & details",
386         'start': "vsname(s)\n\tstart vservers",
387         'stop' : "vsname(s)\n\tstop vservers",
388         'check' : "vsname(s)\n\tsanity check - ip from hostname, netmask, ...",
389         'delete' : "vsname(s)\n\tdelete vservers",
390         'rename' : "oldname newname\n\trename a vserver - both named must be local",
391         'migrate' : "oldname hostname@newname\n\tmigrate a vserver over to a new box\n\tnot implemented yet",
392         }
393
394     def __init__ (self):
395         self.parser=OptionParser()
396         self.args=None
397         self.options=None
398
399     def vserver(self, vsname):
400         return Vserver(vsname,dry_run=self.options.dry_run,verbose=self.options.verbose)
401
402     def probe_star_arg (self, arg):
403         vs=self.vserver(arg)
404         if vs.has_pattern():    return vs.probe_pattern()
405         else:                   return [arg,]
406
407     def read_args_from_file (self):
408         if not self.options.args_from_file: return []
409         try:
410             result=[]
411             raw_args=file(self.options.args_from_file).read().split()
412             for arg in raw_args:
413                 result += self.probe_star_arg(arg)
414             return result
415         except:
416             logger.log('ERROR: Could not open args file %s'%self.options.args_from_file)
417             return []
418
419     def check_usual_args(self):
420         if not self.options.args_from_file and len(self.args)==0: 
421             print ('no arg specified')
422             self.parser.print_help()
423             sys.exit(1)
424         result=[]
425         for arg in self.args: result += self.probe_star_arg(arg)
426         result += self.read_args_from_file()
427         result.sort()
428         return result
429         
430     # return exit code
431     def run (self):
432
433         mode=None
434         for function in VserverTools.modes.keys():
435             if sys.argv[0].find(function) >= 0:
436                 mode = function
437                 mode_desc = VserverTools.modes[mode]
438                 break
439         if not mode:
440             print "Unsupported command",sys.argv[0]
441             print "Supported commands:" + " ".join(VserverTools.modes.keys())
442             sys.exit(1)
443
444         self.parser.set_usage("%prog " + mode_desc)
445         self.parser.add_option('-n','--dry-run',dest='dry_run',action='store_true',default=False,
446                           help="dry run")
447         self.parser.add_option('-v','--verbose',dest='verbose',action='store_true',default=False,
448                           help="be verbose")
449         self.parser.add_option ('-l','--long',action='store_true', dest='long_format',default=False,
450                                 help="show more detailed result")
451         ##########
452         if mode not in ['rename','migrate'] :
453             self.parser.add_option ('-f','--file',action='store',dest='args_from_file',default=None,
454                                help="read args from file")
455
456         (self.options,self.args) = self.parser.parse_args()
457
458         if mode == 'show':
459             for arg in self.check_usual_args():
460                 self.vserver(arg).show(self.options.long_format)
461         elif mode == 'stop':
462             for arg in self.check_usual_args():
463                 vs = self.vserver(arg)
464                 vs.stop()
465                 vs.show(True)
466         elif mode == 'start':
467             for arg in self.check_usual_args():
468                 vs = self.vserver(arg)
469                 vs.start()
470                 vs.show(True)
471         elif mode == 'check':
472             result=True
473             for arg in self.check_usual_args():
474                 if not b2e(self.vserver(arg).check()): result=False
475             return result
476         elif mode == 'delete':
477             result=True
478             for arg in self.check_usual_args():
479                 if not b2e(self.vserver(arg).delete()): result=False
480                 self.vserver(arg).show(True)
481             return result
482         elif mode == 'rename':
483             [x,y]=self.args
484             return b2e(self.vserver(x).rename(y))
485         elif mode == 'migrate':
486             print 'migrate not yet implemented'
487             return 1
488         else:
489             print 'unknown mode %s'%mode
490             return b2e(False)
491
492 if __name__ == '__main__':
493     sys.exit( VserverTools().run() )