welcome to vs-delete
[infrastructure.git] / scripts / vserver_tools.py
1 #!/usr/bin/python
2 #
3 # $Id$
4 # $URL$
5
6 import sys
7 import os.path
8 import pdb
9 import subprocess
10 from optparse import OptionParser
11 import socket
12
13 #
14 # Thierry Parmentelat - INRIA
15 #
16 # class for issuing commands on a box, either local or remote
17 #
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
20 #
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
24 #
25 # also, the copy operations
26 # (*) either do nothing if ran locally
27 # (*) or copy a local file into the remote 'buildname' 
28
29
30 ########################################
31 class Logger:
32
33     def __init__(self):
34         pass
35
36     def log (self,msg,level=0):
37         if level>0: print >> sys.stderr,level*4*'=',
38         print >>sys.stderr, msg
39
40 logger=Logger()
41
42 ########################################
43 # bool_to_exit_code
44 def b2e (b):
45     if b: return 0
46     else: return 1
47 # exit_to_bool_code
48 def e2b (b):
49     return b==0
50
51 ########################################
52 class Shell:
53     def __init__(self,dry_run=False,verbose=False):
54         self.dry_run=dry_run
55         self.verbose=verbose
56
57     def header (self,message):
58         logger.log(message,level=1)
59
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
66
67     # return an exit code
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)
76             return 0
77         else:
78             helper='RUNNING'
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)
89             
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) + ')'
97             return ""
98         if not trash_stderr:
99             return subprocess.Popen(argv,stdout=subprocess.PIPE).communicate()[0]
100         else:
101             return subprocess.Popen(argv,stdout=subprocess.PIPE,stderr=file('/dev/null','w')).communicate()[0]
102             
103 ####################
104 class Ssh (Shell):
105     
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'
110         self.key=key
111         self.username=username
112         Shell.__init__(self,dry_run=dry_run,verbose=verbose)
113
114     # inserts a backslash before each occurence of the following chars
115     # \ " ' < > & | ; ( ) $ * ~ 
116     @staticmethod
117     def backslash_shell_specials (command):
118         result=''
119         for char in command:
120             if char in "\\\"'<>&|;()$*~":
121                 result +='\\'+char
122             else:
123                 result +=char
124         return result
125
126     # check main IP address against the provided hostname
127     @staticmethod
128     def is_local_hostname (hostname):
129         if hostname == "localhost":
130             return True
131         try:
132             local_ip = socket.gethostbyname(socket.gethostname())
133             remote_ip = socket.gethostbyname(hostname)
134             return local_ip==remote_ip
135         except:
136             logger.log("WARNING : something wrong in is_local_hostname with hostname=%s"%hostname)
137             return False
138
139     @staticmethod
140     def canonical_name (hostname):
141         try:
142             ip=socket.gethostbyname(hostname)
143             return socket.gethostbyaddr(ip)[0]
144         except:
145             logger.log( "WARNING - could not find canonical name for host %s"%hostname)
146             return hostname
147
148
149     ########## building argv
150     def is_local(self):
151         return Ssh.is_local_hostname(self.hostname)
152      
153     def key_argv (self):
154         if not self.key:
155             return []
156         return ["-i",self.key]
157
158     def hostname_arg (self):
159         if not self.username:
160             return self.hostname
161         else:
162             return "%s@%s"%(self.username,self.hostname)
163     
164     std_options="-o BatchMode=yes -o StrictHostKeyChecking=no -o CheckHostIP=no -o ConnectTimeout=5"
165     
166     # command gets run on the right box
167     def argv (self, argv, keep_stdin=False):
168         if self.is_local():
169             return argv
170         ssh_argv = ["ssh"]
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]
177         ssh_argv += argv
178         return ssh_argv
179
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)
184
185 ############################################################
186 class Vserver:
187
188     def __init__ (self, name, dry_run=False,verbose=False):
189         self.dry_run=dry_run
190         self.verbose=verbose
191         self._running_state=None
192         self._xid=None
193         self._autostart=None
194         try:
195             (self.name,hostname)=name.split("@")
196         except:
197             (self.name,hostname)=(name,'localhost')
198         self.set_hostname(hostname)
199
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)
203
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))
210
211     # cache run mode
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,
217                                     force_dry_run=True)
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
222
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 '-'
227         else:                                   return '+'
228
229     # cache xid
230     def xid (self):
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()
235         return self._xid
236
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
243
244     def stop (self):
245         # uncache
246         self._running_state=None
247         return e2b(self.ssh.run("vserver %s stop"%self.name,
248                                 message='Stopping %s'%self.name))
249
250     def start (self):
251         # uncache
252         self._running_state=None
253         return e2b(self.ssh.run("vserver %s start"%self.name,
254                                 message='Starting %s'%self.name))
255
256     def printable_simple(self): return self.name + '@' + self.hostname
257     def printable_dbg(self): 
258         autostart_part='<*>'
259         if not self.autostart(): autostart_part='< >'
260         return autostart_part+" (%s) [%s] %s@%s"%(self.running_char(),self.xid(),self.name,self.hostname)
261     def printable(self): 
262         # force fetch cache info
263         self.running_state()
264         self.xid()
265         self.autostart()
266         return self.printable_dbg()
267
268     def is_local (self):
269         return self.ssh.is_local()
270
271     # does it have a pattern
272     def has_pattern (self):
273         return self.name.find('*') >= 0
274
275     # probe it
276     def probe_pattern (self):
277         pattern=self.name
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()]
282
283     #################### toplevel methods for main
284     def show (self, long_format):
285         if long_format:
286             print self.printable()
287         else:
288             print self.printable_simple()
289
290     def check(self):
291         result=True
292         ssh=self.ssh
293         name=self.name
294         hostname=ssh.backquote("cat /etc/vservers/%(name)s/uts/nodename"%locals()).strip()
295         if not hostname: 
296             self.warning('no uts/nodename')
297         ip=ssh.backquote("cat /etc/vservers/%(name)s/interfaces/0/ip"%locals()).strip()
298         if not ip: 
299             self.error('no interfaces/0/ip')
300             result=False
301         try:
302             expected_ip=socket.gethostbyname(hostname)
303             if expected_ip == ip:
304                 self.ok('hostname %s maps to IP %s'%(hostname,ip))
305             else:
306                 self.error('IP mismatch, current %s -- expected %s'%(ip,expected_ip))
307                 result=False
308         except:
309             self.warning ('could not resolve hostname <%s>'%hostname)
310             expected_ip='255.255.255.255'
311             result=False
312         mask_path="/etc/vservers/%(name)s/interfaces/mask"%locals()
313         mask_exists = e2b(ssh.run('ls %s'%mask_path,trash_stderr=True))
314         if mask_exists:
315             self.error('found a mask file in %s'%(self.printable(),mask_path))
316             result=False
317         else:
318             self.ok('no mask file %s'%mask_path)
319         return result
320         
321     def delete (self):
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))
325
326     # renaming
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
333             else:
334                 logger.log('ERROR:rename cannot rename (%s->%s) - must be on same box'%(self.hostname, target.hostname))
335                 return False
336         logger.log("%s into %s"%(self.printable(),target.printable_simple()),level=2)
337         
338         ssh=self.ssh
339         oldname=self.name
340         newname=target.name
341         xid=self.xid()
342         currently_running=self.running_state()=='start'
343         result=True
344         if currently_running:
345             result = result and self.stop()
346         result = result \
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()
373         return result
374             
375 class VserverTools:
376
377     modes={ 
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",
385         }
386
387     def __init__ (self):
388         self.parser=OptionParser()
389         self.args=None
390         self.options=None
391
392     def vserver(self, vsname):
393         return Vserver(vsname,dry_run=self.options.dry_run,verbose=self.options.verbose)
394
395     def probe_star_arg (self, arg):
396         vs=self.vserver(arg)
397         if vs.has_pattern():    return vs.probe_pattern()
398         else:                   return [arg,]
399
400     def read_args_from_file (self):
401         if not self.options.args_from_file: return []
402         try:
403             result=[]
404             raw_args=file(self.options.args_from_file).read().split()
405             for arg in raw_args:
406                 result += self.probe_star_arg(arg)
407             return result
408         except:
409             logger.log('ERROR: Could not open args file %s'%self.options.args_from_file)
410             return []
411
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()
416             sys.exit(1)
417         result=[]
418         for arg in self.args: result += self.probe_star_arg(arg)
419         result += self.read_args_from_file()
420         result.sort()
421         return result
422         
423     # return exit code
424     def run (self):
425
426         mode=None
427         for function in VserverTools.modes.keys():
428             if sys.argv[0].find(function) >= 0:
429                 mode = function
430                 mode_desc = VserverTools.modes[mode]
431                 break
432         if not mode:
433             print "Unsupported command",sys.argv[0]
434             print "Supported commands:" + " ".join(VserverTools.modes.keys())
435             sys.exit(1)
436
437         self.parser.set_usage("%prog " + mode_desc)
438         self.parser.add_option('-n','--dry-run',dest='dry_run',action='store_true',default=False,
439                           help="dry run")
440         self.parser.add_option('-v','--verbose',dest='verbose',action='store_true',default=False,
441                           help="be verbose")
442         self.parser.add_option ('-l','--long',action='store_true', dest='long_format',default=False,
443                                 help="show more detailed result")
444         ##########
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")
448
449         (self.options,self.args) = self.parser.parse_args()
450
451         if mode == 'show':
452             for arg in self.check_usual_args():
453                 self.vserver(arg).show(self.options.long_format)
454         elif mode == 'stop':
455             for arg in self.check_usual_args():
456                 vs = self.vserver(arg)
457                 vs.stop()
458                 vs.show(True)
459         elif mode == 'start':
460             for arg in self.check_usual_args():
461                 vs = self.vserver(arg)
462                 vs.start()
463                 vs.show(True)
464         elif mode == 'check':
465             result=True
466             for arg in self.check_usual_args():
467                 if not b2e(self.vserver(arg).check()): result=False
468             return result
469         elif mode == 'delete':
470             result=True
471             for arg in self.check_usual_args():
472                 if not b2e(self.vserver(arg).delete()): result=False
473                 self.vserver(arg).show(True)
474             return result
475         elif mode == 'rename':
476             [x,y]=self.args
477             return b2e(self.vserver(x).rename(y))
478         elif mode == 'migrate':
479             print 'migrate not yet implemented'
480             return 1
481         else:
482             print 'unknown mode %s'%mode
483             return b2e(False)
484
485 if __name__ == '__main__':
486     sys.exit( VserverTools().run() )