4be0646eacf1a08496725f28d2bbd2cd622578a5
[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_node (self,nodename):
120         for site in self.plc_spec['sites']:
121             for node in site['nodes']:
122                 if node['node_fields']['hostname'] == nodename:
123                     return (site,node)
124         raise Exception,"Cannot locate node %s"%nodename
125         
126     def locate_key (self,keyname):
127         for key in self.plc_spec['keys']:
128             if key['name'] == keyname:
129                 return key
130         raise Exception,"Cannot locate key %s"%keyname
131         
132     # this should be run on the nodes' host_box, not locally to the plc
133     def kill_all_vmwares(self):
134         utils.header('Killing any running vmware or vmplayer instance')
135         utils.system('pgrep vmware | xargs -r kill')
136         utils.system('pgrep vmplayer | xargs -r kill ')
137         utils.system('pgrep vmware | xargs -r kill -9')
138         utils.system('pgrep vmplayer | xargs -r kill -9')
139
140     def kill_all_qemus(self):
141         for site_spec in self.plc_spec['sites']:
142             test_site = TestSite (self,site_spec)
143             for node_spec in site_spec['nodes']:
144                 TestNode (self,test_site,node_spec).stop_qemu()
145                     
146     def clear_ssh_config (self,options):
147         # install local ssh_config file as root's .ssh/config - ssh should be quiet
148         # dir might need creation first
149         self.run_in_guest("mkdir /root/.ssh")
150         self.run_in_guest("chmod 700 /root/.ssh")
151         # this does not work - > redirection somehow makes it until an argument to cat
152         #self.run_in_guest_piped("cat ssh_config","cat > /root/.ssh/config")
153         self.copy_in_guest("ssh_config","/root/.ssh/config",True)
154         return True
155             
156     #################### step methods
157
158     ### uninstall
159     def uninstall_chroot(self,options):
160         self.run_in_host('service plc safestop')
161         #####detecting the last myplc version installed and remove it
162         self.run_in_host('rpm -e myplc')
163         ##### Clean up the /plc directory
164         self.run_in_host('rm -rf  /plc/data')
165         ##### stop any running vservers
166         self.run_in_host('for vserver in $(ls /vservers/* | sed -e s,/vservers/,,) ; do vserver $vserver stop ; done')
167         return True
168
169     def uninstall_vserver(self,options):
170         self.run_in_host("vserver --silent %s delete"%self.vservername)
171         return True
172
173     def uninstall(self,options):
174         # if there's a chroot-based myplc running, and then a native-based myplc is being deployed
175         # it sounds safer to have the former uninstalled too
176         # now the vserver method cannot be invoked for chroot instances as vservername is required
177         if self.vserver:
178             self.uninstall_vserver(options)
179             self.uninstall_chroot(options)
180         else:
181             self.uninstall_chroot(options)
182         return True
183
184     ### install
185     def install_chroot(self,options):
186         # nothing to do
187         return True
188
189     # xxx this would not work with hostname != localhost as mylc-init-vserver was extracted locally
190     def install_vserver(self,options):
191         # we need build dir for vtest-init-vserver
192         if self.is_local():
193             # a full path for the local calls
194             build_dir=self.path+"/build"
195         else:
196             # use a standard name - will be relative to HOME 
197             build_dir="tests-system-build"
198         build_checkout = "svn checkout %s %s"%(options.build_url,build_dir)
199         if self.run_in_host(build_checkout) != 0:
200             raise Exception,"Cannot checkout build dir"
201         # the repo url is taken from myplc-url 
202         # with the last two steps (i386/myplc...) removed
203         repo_url = options.myplc_url
204         repo_url = os.path.dirname(repo_url)
205         repo_url = os.path.dirname(repo_url)
206         create_vserver="%s/vtest-init-vserver.sh %s %s -- --interface eth0:%s"%\
207             (build_dir,self.vservername,repo_url,self.vserverip)
208         if self.run_in_host(create_vserver) != 0:
209             raise Exception,"Could not create vserver for %s"%self.vservername
210         return True
211
212     def install(self,options):
213         if self.vserver:
214             return self.install_vserver(options)
215         else:
216             return self.install_chroot(options)
217
218     ### install_rpm
219     def install_rpm_chroot(self,options):
220         utils.header('Installing from %s'%options.myplc_url)
221         url=options.myplc_url
222         self.run_in_host('rpm -Uvh '+url)
223         self.run_in_host('service plc mount')
224         return True
225
226     def install_rpm_vserver(self,options):
227         self.run_in_guest("yum -y install myplc-native")
228         return True
229
230     def install_rpm(self,options):
231         if self.vserver:
232             return self.install_rpm_vserver(options)
233         else:
234             return self.install_rpm_chroot(options)
235
236     ### 
237     def configure(self,options):
238         tmpname='%s/%s.plc-config-tty'%(options.path,self.name())
239         fileconf=open(tmpname,'w')
240         for var in [ 'PLC_NAME',
241                      'PLC_ROOT_PASSWORD',
242                      'PLC_ROOT_USER',
243                      'PLC_MAIL_ENABLED',
244                      'PLC_MAIL_SUPPORT_ADDRESS',
245                      'PLC_DB_HOST',
246                      'PLC_API_HOST',
247                      'PLC_WWW_HOST',
248                      'PLC_BOOT_HOST',
249                      'PLC_NET_DNS1',
250                      'PLC_NET_DNS2']:
251             fileconf.write ('e %s\n%s\n'%(var,self.plc_spec[var]))
252         fileconf.write('w\n')
253         fileconf.write('q\n')
254         fileconf.close()
255         utils.system('cat %s'%tmpname)
256         self.run_in_guest_piped('cat %s'%tmpname,'plc-config-tty')
257         utils.system('rm %s'%tmpname)
258         return True
259
260     # the chroot install is slightly different to this respect
261     def start(self, options):
262         if self.vserver:
263             self.run_in_guest('service plc start')
264         else:
265             self.run_in_host('service plc start')
266         return True
267         
268     def stop(self, options):
269         if self.vserver:
270             self.run_in_guest('service plc stop')
271         else:
272             self.run_in_host('service plc stop')
273         return True
274         
275     # could use a TestKey class
276     def store_keys(self, options):
277         for key_spec in self.plc_spec['keys']:
278             TestKey(self,key_spec).store_key()
279         return True
280
281     def clean_keys(self, options):
282         utils.system("rm -rf %s/keys/"%self.path)
283
284     def sites (self,options):
285         return self.do_sites(options)
286     
287     def clean_sites (self,options):
288         return self.do_sites(options,action="delete")
289     
290     def do_sites (self,options,action="add"):
291         for site_spec in self.plc_spec['sites']:
292             test_site = TestSite (self,site_spec)
293             if (action != "add"):
294                 utils.header("Deleting site %s in %s"%(test_site.name(),self.name()))
295                 test_site.delete_site()
296                 # deleted with the site
297                 #test_site.delete_users()
298                 continue
299             else:
300                 utils.header("Creating site %s & users in %s"%(test_site.name(),self.name()))
301                 test_site.create_site()
302                 test_site.create_users()
303         return True
304
305     def nodes (self, options):
306         return self.do_nodes(options)
307     def clean_nodes (self, options):
308         return self.do_nodes(options,action="delete")
309
310     def do_nodes (self, options,action="add"):
311         for site_spec in self.plc_spec['sites']:
312             test_site = TestSite (self,site_spec)
313             if action != "add":
314                 utils.header("Deleting nodes in site %s"%test_site.name())
315                 for node_spec in site_spec['nodes']:
316                     test_node=TestNode(self,test_site,node_spec)
317                     utils.header("Deleting %s"%test_node.name())
318                     test_node.delete_node()
319             else:
320                 utils.header("Creating nodes for site %s in %s"%(test_site.name(),self.name()))
321                 for node_spec in site_spec['nodes']:
322                     utils.show_spec('Creating node %s'%node_spec,node_spec)
323                     test_node = TestNode (self,test_site,node_spec)
324                     test_node.create_node ()
325         return True
326
327     # create nodegroups if needed, and populate
328     # no need for a clean_nodegroups if we are careful enough
329     def nodegroups (self, options):
330         # 1st pass to scan contents
331         groups_dict = {}
332         for site_spec in self.plc_spec['sites']:
333             test_site = TestSite (self,site_spec)
334             for node_spec in site_spec['nodes']:
335                 test_node=TestNode (self,test_site,node_spec)
336                 if node_spec.has_key('nodegroups'):
337                     nodegroupnames=node_spec['nodegroups']
338                     if isinstance(nodegroupnames,StringTypes):
339                         nodegroupnames = [ nodegroupnames ]
340                     for nodegroupname in nodegroupnames:
341                         if not groups_dict.has_key(nodegroupname):
342                             groups_dict[nodegroupname]=[]
343                         groups_dict[nodegroupname].append(test_node.name())
344         auth=self.auth_root()
345         for (nodegroupname,group_nodes) in groups_dict.iteritems():
346             try:
347                 self.server.GetNodeGroups(auth,{'name':nodegroupname})[0]
348             except:
349                 self.server.AddNodeGroup(auth,{'name':nodegroupname})
350             for node in group_nodes:
351                 self.server.AddNodeToNodeGroup(auth,node,nodegroupname)
352         return True
353
354     def all_hostnames (self) :
355         hostnames = []
356         for site_spec in self.plc_spec['sites']:
357             hostnames += [ node_spec['node_fields']['hostname'] \
358                            for node_spec in site_spec['nodes'] ]
359         return hostnames
360
361     # gracetime : during the first <gracetime> minutes nothing gets printed
362     def do_check_nodes (self, minutes, gracetime=2):
363
364         # compute timeout
365         timeout = datetime.datetime.now()+datetime.timedelta(minutes=minutes)
366         graceout = datetime.datetime.now()+datetime.timedelta(minutes=gracetime)
367
368         # the nodes that haven't checked yet - start with a full list and shrink over time
369         tocheck = self.all_hostnames()
370         utils.header("checking nodes %r"%tocheck)
371         # create a dict hostname -> status
372         status = dict ( [ (hostname,'undef') for hostname in tocheck ] )
373
374         while tocheck:
375             # get their status
376             tocheck_status=self.server.GetNodes(self.auth_root(), tocheck, ['hostname','boot_state' ] )
377             # update status
378             for array in tocheck_status:
379                 hostname=array['hostname']
380                 boot_state=array['boot_state']
381                 if boot_state == 'boot':
382                     utils.header ("%s has reached the 'boot' state"%hostname)
383                 else:
384                     # if it's a real node, never mind
385                     (site_spec,node_spec)=self.locate_node(hostname)
386                     test_node = TestNode(self,site_spec,node_spec)
387                     if test_node.is_real():
388                         utils.header("WARNING - Real node %s in %s - ignored"%(hostname,boot_state))
389                         # let's cheat
390                         boot_state = 'boot'
391                     if datetime.datetime.now() > graceout:
392                         utils.header ("%s still in '%s' state"%(hostname,boot_state))
393                 status[hostname] = boot_state
394             # refresh tocheck
395             tocheck = [ hostname for (hostname,boot_state) in status.iteritems() if boot_state != 'boot' ]
396
397             if not tocheck:
398                 return True
399             if datetime.datetime.now() > timeout:
400                 for hostname in tocheck:
401                     utils.header("FAILURE due to %s in '%s' state"%(hostname,status[hostname]))
402                 return False
403             # otherwise, sleep for a while
404             time.sleep(15)
405         # only useful in empty plcs
406         return True
407
408     def check_nodes(self,options):
409         return self.do_check_nodes(minutes=5)
410
411     def bootcd (self, options):
412         for site_spec in self.plc_spec['sites']:
413             test_site = TestSite (self,site_spec)
414             for node_spec in site_spec['nodes']:
415                 test_node=TestNode (self,test_site,node_spec)
416                 test_node.create_boot_cd(options.path)
417         return True
418                 
419     def initscripts (self, options):
420         for initscript in self.plc_spec['initscripts']:
421             utils.show_spec('Adding Initscript in plc %s'%self.plc_spec['name'],initscript)
422             self.server.AddInitScript(self.auth_root(),initscript['initscript_fields'])
423         return True
424
425     def slices (self, options):
426         return self.do_slices()
427
428     def clean_slices (self, options):
429         return self.do_slices("delete")
430
431     def do_slices (self,  action="add"):
432         for slice in self.plc_spec['slices']:
433             site_spec = self.locate_site (slice['sitename'])
434             test_site = TestSite(self,site_spec)
435             test_slice=TestSlice(self,test_site,slice)
436             if action != "add":
437                 utils.header("Deleting slices in site %s"%test_site.name())
438                 test_slice.delete_slice()
439             else:    
440                 utils.show_spec("Creating slice",slice)
441                 test_slice.create_slice()
442                 utils.header('Created Slice %s'%slice['slice_fields']['name'])
443         return True
444         
445     def check_slices(self, options):
446         for slice_spec in self.plc_spec['slices']:
447             site_spec = self.locate_site (slice_spec['sitename'])
448             test_site = TestSite(self,site_spec)
449             test_slice=TestSlice(self,test_site,slice_spec)
450             status=test_slice.do_check_slices()
451             return status
452     
453     def start_nodes (self, options):
454         self.kill_all_vmwares()
455         self.kill_all_qemus()
456         utils.header("Starting vmware nodes")
457         for site_spec in self.plc_spec['sites']:
458             TestSite(self,site_spec).start_nodes (options)
459         return True
460
461     def stop_nodes (self, options):
462         self.kill_all_vmwares ()
463         self.kill_all_qemus()
464         return True
465
466     # returns the filename to use for sql dump/restore, using options.dbname if set
467     def dbfile (self, database, options):
468         # uses options.dbname if it is found
469         try:
470             name=options.dbname
471             if not isinstance(name,StringTypes):
472                 raise Exception
473         except:
474             t=datetime.datetime.now()
475             d=t.date()
476             name=str(d)
477         return "/root/%s-%s.sql"%(database,name)
478
479     def db_dump(self, options):
480         
481         dump=self.dbfile("planetab4",options)
482         self.run_in_guest('pg_dump -U pgsqluser planetlab4 -f '+ dump)
483         utils.header('Dumped planetlab4 database in %s'%dump)
484         return True
485
486     def db_restore(self, options):
487         dump=self.dbfile("planetab4",options)
488         ##stop httpd service
489         self.run_in_guest('service httpd stop')
490         # xxx - need another wrapper
491         self.run_in_guest_piped('echo drop database planetlab4','psql --user=pgsqluser template1')
492         self.run_in_guest('createdb -U postgres --encoding=UNICODE --owner=pgsqluser planetlab4')
493         self.run_in_guest('psql -U pgsqluser planetlab4 -f '+dump)
494         ##starting httpd service
495         self.run_in_guest('service httpd start')
496
497         utils.header('Database restored from ' + dump)