command chaining (ssh, chroot, vserver ...) reviewed
[tests.git] / system / TestPlc.py
1 # $Id$
2 import os, os.path
3 import datetime
4 import time
5 import sys
6 import xmlrpclib
7 import datetime
8 import traceback
9 from types import StringTypes
10
11 import utils
12 from TestSite import TestSite
13 from TestNode import TestNode
14 from TestUser import TestUser
15 from TestKey import TestKey
16 from TestSlice import TestSlice
17
18 # inserts a backslash before each occurence of the following chars
19 # \ " ' < > & | ; ( ) $ *
20 def backslash_shell_specials (command):
21     result=''
22     for char in command:
23         if char in "\\\"'<>&|;()$*":
24             result +='\\'+char
25         else:
26             result +=char
27     return result
28
29 # step methods must take (self, options) and return a boolean
30
31 class TestPlc:
32
33     def __init__ (self,plc_spec):
34         self.plc_spec=plc_spec
35         self.path=os.path.dirname(sys.argv[0])
36         try:
37             self.vserverip=plc_spec['vserverip']
38             self.vservername=plc_spec['vservername']
39             self.url="https://%s:443/PLCAPI/"%plc_spec['vserverip']
40             self.vserver=True
41         except:
42             self.vserver=False
43             self.url="https://%s:443/PLCAPI/"%plc_spec['hostname']
44         utils.header('Using API url %s'%self.url)
45         self.server=xmlrpclib.Server(self.url,allow_none=True)
46         
47     def name(self):
48         name=self.plc_spec['name']
49         if self.vserver:
50             return name+"[%s]"%self.vservername
51         else:
52             return name+"[chroot]"
53
54     def is_local (self):
55         return self.plc_spec['hostname'] == 'localhost'
56
57     # define the API methods on this object through xmlrpc
58     # would help, but not strictly necessary
59     def connect (self):
60         pass
61     
62     # command gets run in the chroot/vserver
63     def host_to_guest(self,command):
64         if self.vserver:
65             return "vserver %s exec %s"%(self.vservername,command)
66         else:
67             return "chroot /plc/root %s"%backslash_shell_specials(command)
68
69     # command gets run on the right box
70     def to_host(self,command):
71         if self.is_local():
72             return command
73         else:
74             return "ssh %s %s"%(self.plc_spec['hostname'],backslash_shell_specials(command))
75
76     def full_command(self,command):
77         return self.to_host(self.host_to_guest(command))
78
79     def run_in_guest (self,command):
80         return utils.system(self.full_command(command))
81     def run_in_host (self,command):
82         return utils.system(self.to_host(command))
83
84     # xxx quick n dirty
85     def run_in_guest_piped (self,local,remote):
86         return utils.system(local+" | "+self.full_command(remote))
87
88     # copy a file to the myplc root image - pass in_data=True if the file must go in /plc/data
89     def copy_in_guest (self, localfile, remotefile, in_data=False):
90         if in_data:
91             chroot_dest="/plc/data"
92         else:
93             chroot_dest="/plc/root"
94         if self.is_local():
95             if not self.vserver:
96                 utils.system("cp %s %s/%s"%(localfile,chroot_dest,remotefile))
97             else:
98                 utils.system("cp %s /vservers/%s/%s"%(localfile,self.vservername,remotefile))
99         else:
100             if not self.vserver:
101                 utils.system("scp %s %s:%s/%s"%(localfile,self.plc_spec['hostname'],chroot_dest,remotefile))
102             else:
103                 utils.system("scp %s %s@/vservers/%s/%s"%(localfile,self.plc_spec['hostname'],self.vservername,remotefile))
104
105     def auth_root (self):
106         return {'Username':self.plc_spec['PLC_ROOT_USER'],
107                 'AuthMethod':'password',
108                 'AuthString':self.plc_spec['PLC_ROOT_PASSWORD'],
109                 'Role' : self.plc_spec['role']
110                 }
111     def locate_site (self,sitename):
112         for site in self.plc_spec['sites']:
113             if site['site_fields']['name'] == sitename:
114                 return site
115             if site['site_fields']['login_base'] == sitename:
116                 return site
117         raise Exception,"Cannot locate site %s"%sitename
118         
119     def locate_key (self,keyname):
120         for key in self.plc_spec['keys']:
121             if key['name'] == keyname:
122                 return key
123         raise Exception,"Cannot locate key %s"%keyname
124         
125     # this should be run on the nodes' host_box, not locally to the plc
126     def kill_all_vmwares(self):
127         utils.header('Killing any running vmware or vmplayer instance')
128         utils.system('pgrep vmware | xargs -r kill')
129         utils.system('pgrep vmplayer | xargs -r kill ')
130         utils.system('pgrep vmware | xargs -r kill -9')
131         utils.system('pgrep vmplayer | xargs -r kill -9')
132
133     def kill_all_qemus(self):
134         for site_spec in self.plc_spec['sites']:
135             test_site = TestSite (self,site_spec)
136             for node_spec in site_spec['nodes']:
137                 TestNode (self,test_site,node_spec).stop_qemu()
138                     
139     def clear_ssh_config (self,options):
140         # install local ssh_config file as root's .ssh/config - ssh should be quiet
141         # dir might need creation first
142         self.run_in_guest("mkdir /root/.ssh")
143         self.run_in_guest("chmod 700 /root/.ssh")
144         # this does not work - > redirection somehow makes it until an argument to cat
145         #self.run_in_guest_piped("cat ssh_config","cat > /root/.ssh/config")
146         self.copy_in_guest("ssh_config","/root/.ssh/config",True)
147         return True
148             
149     #################### step methods
150
151     ### uninstall
152     def uninstall_chroot(self,options):
153         self.run_in_host('service plc safestop')
154         #####detecting the last myplc version installed and remove it
155         self.run_in_host('rpm -e myplc')
156         ##### Clean up the /plc directory
157         self.run_in_host('rm -rf  /plc/data')
158         ##### stop any running vservers
159         self.run_in_host('for vserver in $(ls /vservers/* | sed -e s,/vservers/,,) ; do vserver $vserver stop ; done')
160         return True
161
162     def uninstall_vserver(self,options):
163         self.run_in_host("vserver --silent %s delete"%self.vservername)
164         return True
165
166     def uninstall(self,options):
167         # if there's a chroot-based myplc running, and then a native-based myplc is being deployed
168         # it sounds safer to have the former uninstalled too
169         # now the vserver method cannot be invoked for chroot instances as vservername is required
170         if self.vserver:
171             self.uninstall_vserver(options)
172             self.uninstall_chroot(options)
173         else:
174             self.uninstall_chroot(options)
175         return True
176
177     ### install
178     def install_chroot(self,options):
179         # nothing to do
180         return True
181
182     # xxx this would not work with hostname != localhost as mylc-init-vserver was extracted locally
183     def install_vserver(self,options):
184         # we need build dir for vtest-init-vserver
185         if self.is_local():
186             # a full path for the local calls
187             build_dir=self.path+"/build"
188         else:
189             # use a standard name - will be relative to HOME 
190             build_dir="tests-system-build"
191         build_checkout = "svn checkout %s %s"%(options.build_url,build_dir)
192         if self.run_in_host(build_checkout) != 0:
193             raise Exception,"Cannot checkout build dir"
194         # the repo url is taken from myplc-url 
195         # with the last two steps (i386/myplc...) removed
196         repo_url = options.myplc_url
197         repo_url = os.path.dirname(repo_url)
198         repo_url = os.path.dirname(repo_url)
199         create_vserver="%s/vtest-init-vserver.sh %s %s -- --interface eth0:%s"%\
200             (build_dir,self.vservername,repo_url,self.vserverip)
201         if self.run_in_host(create_vserver) != 0:
202             raise Exception,"Could not create vserver for %s"%self.vservername
203         return True
204
205     def install(self,options):
206         if self.vserver:
207             return self.install_vserver(options)
208         else:
209             return self.install_chroot(options)
210
211     ### install_rpm
212     def install_rpm_chroot(self,options):
213         utils.header('Installing from %s'%options.myplc_url)
214         url=options.myplc_url
215         self.run_in_host('rpm -Uvh '+url)
216         self.run_in_host('service plc mount')
217         return True
218
219     def install_rpm_vserver(self,options):
220         self.run_in_guest("yum -y install myplc-native")
221         return True
222
223     def install_rpm(self,options):
224         if self.vserver:
225             return self.install_rpm_vserver(options)
226         else:
227             return self.install_rpm_chroot(options)
228
229     ### 
230     def configure(self,options):
231         tmpname='%s/%s.plc-config-tty'%(options.path,self.name())
232         fileconf=open(tmpname,'w')
233         for var in [ 'PLC_NAME',
234                      'PLC_ROOT_PASSWORD',
235                      'PLC_ROOT_USER',
236                      'PLC_MAIL_ENABLED',
237                      'PLC_MAIL_SUPPORT_ADDRESS',
238                      'PLC_DB_HOST',
239                      'PLC_API_HOST',
240                      'PLC_WWW_HOST',
241                      'PLC_BOOT_HOST',
242                      'PLC_NET_DNS1',
243                      'PLC_NET_DNS2']:
244             fileconf.write ('e %s\n%s\n'%(var,self.plc_spec[var]))
245         fileconf.write('w\n')
246         fileconf.write('q\n')
247         fileconf.close()
248         utils.system('cat %s'%tmpname)
249         self.run_in_guest_piped('cat %s'%tmpname,'plc-config-tty')
250         utils.system('rm %s'%tmpname)
251         return True
252
253     # the chroot install is slightly different to this respect
254     def start(self, options):
255         if self.vserver:
256             self.run_in_guest('service plc start')
257         else:
258             self.run_in_host('service plc start')
259         return True
260         
261     def stop(self, options):
262         if self.vserver:
263             self.run_in_guest('service plc stop')
264         else:
265             self.run_in_host('service plc stop')
266         return True
267         
268     # could use a TestKey class
269     def store_keys(self, options):
270         for key_spec in self.plc_spec['keys']:
271             TestKey(self,key_spec).store_key()
272         return True
273
274     def clean_keys(self, options):
275         utils.system("rm -rf %s/keys/"%self.path)
276
277     def sites (self,options):
278         return self.do_sites(options)
279     
280     def clean_sites (self,options):
281         return self.do_sites(options,action="delete")
282     
283     def do_sites (self,options,action="add"):
284         for site_spec in self.plc_spec['sites']:
285             test_site = TestSite (self,site_spec)
286             if (action != "add"):
287                 utils.header("Deleting site %s in %s"%(test_site.name(),self.name()))
288                 test_site.delete_site()
289                 # deleted with the site
290                 #test_site.delete_users()
291                 continue
292             else:
293                 utils.header("Creating site %s & users in %s"%(test_site.name(),self.name()))
294                 test_site.create_site()
295                 test_site.create_users()
296         return True
297
298     def nodes (self, options):
299         return self.do_nodes(options)
300     def clean_nodes (self, options):
301         return self.do_nodes(options,action="delete")
302
303     def do_nodes (self, options,action="add"):
304         for site_spec in self.plc_spec['sites']:
305             test_site = TestSite (self,site_spec)
306             if action != "add":
307                 utils.header("Deleting nodes in site %s"%test_site.name())
308                 for node_spec in site_spec['nodes']:
309                     test_node=TestNode(self,test_site,node_spec)
310                     utils.header("Deleting %s"%test_node.name())
311                     test_node.delete_node()
312             else:
313                 utils.header("Creating nodes for site %s in %s"%(test_site.name(),self.name()))
314                 for node_spec in site_spec['nodes']:
315                     utils.show_spec('Creating node %s'%node_spec,node_spec)
316                     test_node = TestNode (self,test_site,node_spec)
317                     test_node.create_node ()
318         return True
319
320     # create nodegroups if needed, and populate
321     # no need for a clean_nodegroups if we are careful enough
322     def nodegroups (self, options):
323         # 1st pass to scan contents
324         groups_dict = {}
325         for site_spec in self.plc_spec['sites']:
326             test_site = TestSite (self,site_spec)
327             for node_spec in site_spec['nodes']:
328                 test_node=TestNode (self,test_site,node_spec)
329                 if node_spec.has_key('nodegroups'):
330                     nodegroupnames=node_spec['nodegroups']
331                     if isinstance(nodegroupnames,StringTypes):
332                         nodegroupnames = [ nodegroupnames ]
333                     for nodegroupname in nodegroupnames:
334                         if not groups_dict.has_key(nodegroupname):
335                             groups_dict[nodegroupname]=[]
336                         groups_dict[nodegroupname].append(test_node.name())
337         auth=self.auth_root()
338         for (nodegroupname,group_nodes) in groups_dict.iteritems():
339             try:
340                 self.server.GetNodeGroups(auth,{'name':nodegroupname})[0]
341             except:
342                 self.server.AddNodeGroup(auth,{'name':nodegroupname})
343             for node in group_nodes:
344                 self.server.AddNodeToNodeGroup(auth,node,nodegroupname)
345         return True
346
347     def all_hostnames (self) :
348         hostnames = []
349         for site_spec in self.plc_spec['sites']:
350             hostnames += [ node_spec['node_fields']['hostname'] \
351                            for node_spec in site_spec['nodes'] ]
352         return hostnames
353
354     # gracetime : during the first <gracetime> minutes nothing gets printed
355     def do_check_nodes (self, minutes, gracetime=2):
356
357         # compute timeout
358         timeout = datetime.datetime.now()+datetime.timedelta(minutes=minutes)
359         graceout = datetime.datetime.now()+datetime.timedelta(minutes=gracetime)
360
361         # the nodes that haven't checked yet - start with a full list and shrink over time
362         tocheck = self.all_hostnames()
363         utils.header("checking nodes %r"%tocheck)
364         # create a dict hostname -> status
365         status = dict ( [ (hostname,'undef') for hostname in tocheck ] )
366
367         while tocheck:
368             # get their status
369             tocheck_status=self.server.GetNodes(self.auth_root(), tocheck, ['hostname','boot_state' ] )
370             # update status
371             for array in tocheck_status:
372                 hostname=array['hostname']
373                 boot_state=array['boot_state']
374                 if boot_state == 'boot':
375                     utils.header ("%s has reached the 'boot' state"%hostname)
376                 else:
377                     if datetime.datetime.now() > graceout:
378                         utils.header ("%s still in '%s' state"%(hostname,boot_state))
379                 status[hostname] = boot_state
380             # refresh tocheck
381             tocheck = [ hostname for (hostname,boot_state) in status.iteritems() if boot_state != 'boot' ]
382
383             if not tocheck:
384                 return True
385             if datetime.datetime.now() > timeout:
386                 for hostname in tocheck:
387                     utils.header("FAILURE due to %s in '%s' state"%(hostname,status[hostname]))
388                 return False
389             # otherwise, sleep for a while
390             time.sleep(15)
391         # only useful in empty plcs
392         return True
393
394     def check_nodes(self,options):
395         return self.do_check_nodes(minutes=5)
396
397     def bootcd (self, options):
398         for site_spec in self.plc_spec['sites']:
399             test_site = TestSite (self,site_spec)
400             for node_spec in site_spec['nodes']:
401                 test_node=TestNode (self,test_site,node_spec)
402                 test_node.create_boot_cd(options.path)
403         return True
404                 
405     def initscripts (self, options):
406         for initscript in self.plc_spec['initscripts']:
407             utils.show_spec('Adding Initscript in plc %s'%self.plc_spec['name'],initscript)
408             self.server.AddInitScript(self.auth_root(),initscript['initscript_fields'])
409         return True
410
411     def slices (self, options):
412         return self.do_slices()
413
414     def clean_slices (self, options):
415         return self.do_slices("delete")
416
417     def do_slices (self,  action="add"):
418         for slice in self.plc_spec['slices']:
419             site_spec = self.locate_site (slice['sitename'])
420             test_site = TestSite(self,site_spec)
421             test_slice=TestSlice(self,test_site,slice)
422             if action != "add":
423                 utils.header("Deleting slices in site %s"%test_site.name())
424                 test_slice.delete_slice()
425             else:    
426                 utils.show_spec("Creating slice",slice)
427                 test_slice.create_slice()
428                 utils.header('Created Slice %s'%slice['slice_fields']['name'])
429         return True
430         
431     def check_slices(self, options):
432         for slice_spec in self.plc_spec['slices']:
433             site_spec = self.locate_site (slice_spec['sitename'])
434             test_site = TestSite(self,site_spec)
435             test_slice=TestSlice(self,test_site,slice_spec)
436             status=test_slice.do_check_slices()
437             return status
438     
439     def start_nodes (self, options):
440         self.kill_all_vmwares()
441         self.kill_all_qemus()
442         utils.header("Starting vmware nodes")
443         for site_spec in self.plc_spec['sites']:
444             TestSite(self,site_spec).start_nodes (options)
445         return True
446
447     def stop_nodes (self, options):
448         self.kill_all_vmwares ()
449         self.kill_all_qemus()
450         return True
451
452     # returns the filename to use for sql dump/restore, using options.dbname if set
453     def dbfile (self, database, options):
454         # uses options.dbname if it is found
455         try:
456             name=options.dbname
457             if not isinstance(name,StringTypes):
458                 raise Exception
459         except:
460             t=datetime.datetime.now()
461             d=t.date()
462             name=str(d)
463         return "/root/%s-%s.sql"%(database,name)
464
465     def db_dump(self, options):
466         
467         dump=self.dbfile("planetab4",options)
468         self.run_in_guest('pg_dump -U pgsqluser planetlab4 -f '+ dump)
469         utils.header('Dumped planetlab4 database in %s'%dump)
470         return True
471
472     def db_restore(self, options):
473         dump=self.dbfile("planetab4",options)
474         ##stop httpd service
475         self.run_in_guest('service httpd stop')
476         # xxx - need another wrapper
477         self.run_in_guest_piped('echo drop database planetlab4','psql --user=pgsqluser template1')
478         self.run_in_guest('createdb -U postgres --encoding=UNICODE --owner=pgsqluser planetlab4')
479         self.run_in_guest('psql -U pgsqluser planetlab4 -f '+dump)
480         ##starting httpd service
481         self.run_in_guest('service httpd start')
482
483         utils.header('Database restored from ' + dump)