Fixed bug in devicespec parsing
[qa-tools:ots.git] / ots.server / ots / server / xmlrpc / public.py
1 # ***** BEGIN LICENCE BLOCK *****
2 # This file is part of OTS
3 #
4 # Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
5 #
6 # Contact: Mikko Makinen <mikko.al.makinen@nokia.com>
7 #
8 # This library is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU Lesser General Public License
10 # version 2.1 as published by the Free Software Foundation.
11 #
12 # This library is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # Lesser General Public License for more details.
16 #
17 # You should have received a copy of the GNU Lesser General Public
18 # License along with this library; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
20 # 02110-1301 USA
21 # ***** END LICENCE BLOCK *****
22
23 """
24 ots public xml-rpc interface
25 """
26
27 import logging
28 import logging.handlers
29 import datetime
30 import copy
31 import re
32
33 from multiprocessing import Queue
34 from ots.server.testrun_host.testrun_host import TestrunHost
35 from ots.server.xmlrpc.handleprocesses import HandleProcesses
36
37 # Extension point mechanism
38 try:
39     from ots_extensions import extension_points
40 except ImportError: # If no extension points found, use the empty default
41     from ots.server.testrun_host import \
42         example_extension_points as extension_points
43
44 try:    
45     from ots_extensions import ots_config
46 except ImportError: # If no custom config found, use the default
47     from ots.server.testrun_host import default_ots_config as ots_config
48
49
50 REQUEST_ERROR = 'ERROR'
51 REQUEST_FAIL = 'FAIL'
52 REQUEST_PASS = 'PASS'
53
54 # Django Models are metaclasses 
55 # pylint: disable-msg=E1101
56
57 #############################################
58 # PUBLIC METHODS
59 #############################################
60
61 def request_sync(program, request, notify_list, options):
62     """
63     Fires the test request and waits for it to finish
64
65     @param program: program
66     @type program: C{string}
67
68     @param request: BUILD request id 
69     @type request: C{string}
70
71     @param notify_list: Email addresses for notifications 
72     @type notify_list: C{list}
73
74     @param options: Parameters that affect the test request behaviour 
75     @type options: C{dict}
76
77     @rtype: C{string}
78     @return: Pass / Fail or Error
79     """
80
81     #Important: We are relaxed about what is supplied by the client.
82     #Deduce as much as possible, apply defaults for missing parameters 
83
84     #Unpack the necessary parameters out of the options dict
85     image_url = options.get("image", '')
86     rootstrap_url = options.get("rootstrap", '')
87     test_packages = _string_2_list(options.get("packages",""))
88     # TODO: This is for unit testing purposes. Remove if possible.
89     is_executed =  options.get('execute') != 'false'
90
91     try:
92         options = _repack_options(options)
93     except IndexError:
94         return REQUEST_ERROR
95
96     if not is_executed:
97         return REQUEST_ERROR
98
99     # Create process handler and testrun and start them
100     process_handler = HandleProcesses()
101     testrun_list, process_queues =  _create_testruns(options, request, \
102                                         program, notify_list, test_packages, \
103                                         image_url, rootstrap_url)
104     for testrun in testrun_list:
105         process_handler.add_process(_execute_testrun, testrun)
106
107     # Check that we have tasks to run
108     if not process_handler.child_processes:
109         return REQUEST_ERROR
110
111     # Start processes ...
112     process_handler.start_processes()
113     # Read process queues
114     results = _read_queues(process_queues)
115     # Join processes
116     process_handler.join_processes()
117
118     # Check return values from testruns
119     return _check_testruns_result_values(results.values())
120
121 ###########################################
122 # HELPER METHODS
123 ###########################################
124
125 def _execute_testrun(pq, request, program, current_options, notify_list,
126                      test_packages, image_url, rootstrap_url):
127     """
128     Starting point for process that handles testrun. Starts logging, execute
129     _run_test and puts result to queue    
130
131     @param pq: Process queue
132     @type pq: L{multiprocess.Queue}
133
134     @param request: request id 
135     @type request: C{string}
136
137     @param program: Software program 
138     @type program: C{string}
139
140     @param current_options: Parameters that affect the test request behaviour 
141     @type current_options: C{dict}
142
143     @param notify_list: Email addresses for notifications 
144     @type notify_list: C{list}
145     
146     @param test_packages: A list of test packages. May be empty list.
147     @type test_packages: C{list}
148
149     @param image_url: The url of the image
150     @type image_url: C{string}
151
152     @param rootstrap_url: The url of the roostrap
153     @type rootstrap_url: C{string}
154     """
155     # Default result
156     result = REQUEST_ERROR
157
158     testrun_id = extension_points.create_testrun_id(program, request, \
159                                                     current_options)
160
161     if testrun_id is not None:
162         #Now we have the id the logging can commence
163         _initialize_logger(request, testrun_id)
164         log = logging.getLogger(__name__)
165         log.info(("Incoming request: program: %s,"\
166                   " request: %s, notify_list: %s, "\
167                   "options: %s") %\
168                   (program, request, notify_list, current_options))
169
170         result = _run_test(log, request, testrun_id,
171                            program, current_options, notify_list,
172                            test_packages, image_url, rootstrap_url)
173
174     pq.put({testrun_id : result})
175
176 def _read_queues(process_queues):
177     """
178     Returns testrun_id and result pairs
179
180     @param process_queues: List that contains queues used by processes
181     @type process_queues: C{list}
182
183     @rtype: C{dict}
184     @return: Dictionary that contains testrun_id and result pairs
185     """
186     queue_results = {}
187     for process_queue in process_queues:
188         queue_results.update(process_queue.get())
189     return queue_results
190
191 def _check_testruns_result_values(result_values):
192     """
193     Checks overall testrun status and returns value
194
195     @param result_values: List containing result values from executed testruns
196     @type result_values: C{list}
197
198     @rtype: C{list} consisting of C{string}
199     @return: The converted string
200     """
201     if REQUEST_ERROR in result_values:
202         return REQUEST_ERROR
203     elif REQUEST_FAIL in result_values:
204         return REQUEST_FAIL
205     return REQUEST_PASS
206
207 def _create_testruns(options, request, program, notify_list,
208                     test_packages, image_url, rootstrap_url):
209     """
210     Returns testrun_list and process_queues
211
212     @param options: Dict that contains data for testrun
213     @type options: C{dict}
214
215     @param request: BUILD request id 
216     @type request: C{string}
217
218     @param program: Software program
219     @type program: C{string}
220
221     @param notify_list: Email addresses for notifications 
222     @type notify_list: C{list}
223     
224     @param test_packages: A list of test packages. May be empty list.
225     @type test_packages: C{list}
226
227     @param image_url: The url of the image
228     @type image_url: C{string}
229
230     @param rootstrap_url: The url of the roostrap
231     @type rootstrap_url: C{string}
232
233     @rtype: C{dict}, C(Queue)
234     @return: testrun_list which contains arguments in tuple
235              for each subprocess
236     """
237     testrun_list = []
238     process_queues = []
239
240     if options.get('device'):
241         # Separate devigroups for different testrun_id's
242         for devicespec in options['device']:
243             current_options, pq = _prepare_testrun(options)
244             process_queues.append(pq)
245
246             # Check that we have valid devicespecs
247             if not _validate_devicespecs(devicespec.keys()):
248                 continue
249
250             # Fetch devicespecs
251             updated_specs = dict()
252             if devicespec.get('devicegroup'):
253                 updated_specs['devicegroup'] = devicespec['devicegroup']
254             if devicespec.get('devicename'):
255                 updated_specs['devicename'] = devicespec['devicename']
256             if devicespec.get('deviceid'):
257                 updated_specs['deviceid'] = devicespec['deviceid']
258
259             current_options['device'] = updated_specs
260             testrun_list.append((pq, request, program, current_options, \
261                                  notify_list, test_packages, image_url, \
262                                  rootstrap_url))
263     else:
264         current_options, pq = _prepare_testrun(options)
265         process_queues.append(pq)
266         testrun_list.append((pq, request, program, current_options, \
267                              notify_list, test_packages, image_url, \
268                              rootstrap_url))
269
270     return testrun_list, process_queues
271
272 def _validate_devicespecs(devicespecs):
273     """
274     Returns boolean value based on devicespecs
275     validation
276
277     @param devicespecs: List that contains devicespecs
278     @type options: C{List}
279
280     @rtype: C{Boolean}
281     @return: True if devicespecs are valid, False otherwise
282     """
283     for devicespec in devicespecs:
284         if devicespec not in ['devicegroup', 'devicename', 'deviceid']:
285             return False
286     return True
287
288 def _prepare_testrun(options):
289     """
290     Returns deepcopy of options and process queue
291
292     @param options: Dict that contains data for testrun
293     @type options: C{dict}
294
295     @rtype: C{dict}, C(Queue)
296     @return: deepcopy of dictionary and process queue
297     """
298     return copy.deepcopy(options), Queue()
299
300 def _generate_log_id_string(build_id, testrun_id):
301     """
302     Generates the log file name
303     """
304     request_id = "testrun_%s_request_%s_"% (testrun_id, build_id)
305     request_id += str(datetime.datetime.now()).\
306         replace(' ','_').replace(':','_').replace('.','_')
307     return request_id
308
309 def _initialize_logger(build_id, testrun_id):
310     """
311     initializes the logger
312     """
313     log_id_string = _generate_log_id_string(build_id, testrun_id)
314     log = logging.getLogger()
315     for handler in log.handlers:
316         log.root.removeHandler(handler)
317
318     level = logging.DEBUG
319     format = '%(asctime)s  %(module)-12s %(levelname)-8s %(message)s'
320     hdlr = logging.FileHandler(ots_config.log_directory+log_id_string)
321     hdlr.setLevel(level)
322     fmt = logging.Formatter(format)
323     hdlr.setFormatter(fmt)
324     log.addHandler(hdlr)
325     log.setLevel(level)
326
327     if ots_config.http_logging_enabled:
328         from ots.server.logger.localhandler import LocalHttpHandler
329         httphandler = LocalHttpHandler(testrun_id)
330         httphandler.setLevel(logging.INFO)
331         log.addHandler(httphandler)
332     return log
333
334 def _get_testrun_host(log):
335     """
336     Factory method for TestrunHost
337     """
338     try:
339         host = TestrunHost()
340     except:
341         log.exception("Failed to create TestrunHost")
342         host = None
343     return host
344
345 def _string_2_list(string):
346     """
347     Converts a spaced string to an array
348
349     @param string: The string for conversion  
350     @type string: C{string}
351
352     @rtype: C{list} consisting of C{string}
353     @return: The converted string
354     """
355     if string:
356         spaces = re.compile(r'\s+')
357         return spaces.split(string.strip())
358     else:
359         return []
360
361 def _parse_multiple_devicegroup_specs(string):
362     """
363     Converts a spaced string of form 'devicegroup:one sim:operator1;
364     devicegroup:two sim:operator2' to a dictionary
365
366     @param string: The string for conversion  
367     @type string: C{string}
368
369     @rtype: C{dict} consisting of C{string}
370     @return: The converted string
371     """
372     temp_array = []
373     devicegroups = []
374     for splitted_str in string.split(';'):
375         temp_array.append(splitted_str.split())
376     for devicegroup in temp_array:
377         unit_dict = {}
378         for prop in xrange(len(devicegroup)):
379             prop_pair = devicegroup[prop].split(':', 1)
380             unit_dict.update({prop_pair[0]: prop_pair[1]})
381         devicegroups.append(unit_dict)
382
383     return devicegroups
384
385 def _repack_options(options):
386     """
387     Converts the options as provided by the client (for example build machine)
388     into the form accepted by OTS
389
390     @param options: The options parameters 
391     @type options: C{dict}
392
393     @rtype: C{dict} 
394     @return: The conditioned options parameters
395     """
396     conditioned_options = {}
397     if 'hosttest' in options:
398         conditioned_options["hosttest"] \
399             = _string_2_list(options.get("hosttest",""))
400     if 'engine' in options:
401         conditioned_options["engine"] \
402             = _string_2_list(options.get("engine", ""))
403     if 'device' in options:
404         conditioned_options["device"] \
405             = _parse_multiple_devicegroup_specs(options.get("device", ""))
406     if 'emmc' in options:
407         conditioned_options['emmc'] = options['emmc']
408     if 'email-attachments' in options:
409         if options['email-attachments'] == 'on':
410             conditioned_options['email-attachments'] = True
411         else:
412             conditioned_options['email-attachments'] = False
413
414     #Currently some options are still not defined at interface level
415     #so pass on anything that comes our way
416     options.update(conditioned_options)
417     return options
418
419 def _run_test(log, request, testrun_id, program, options, notify_list,
420               test_packages, image_url, rootstrap_url):
421
422     """
423     Run the test on the TestrunHost
424
425     @param log: The python logger
426     @type log: L{logging.Logger}
427
428     @param request: BUILD request id 
429     @type request: C{string}
430
431     @param testrun_id: The OTS testrun_id
432     @type testrun_id: C{string}
433
434     @param program: Software program
435     @type program: C{string}
436
437     @param options: Parameters that affect the test request behaviour 
438     @type options: C{dict}
439
440     @param notify_list: Email addresses for notifications 
441     @type notify_list: C{list}
442     
443     @param test_packages: A list of test packages. May be empty list.
444     @type test_packages: C{list}
445
446     @param image_url: The url of the image
447     @type image_url: C{string}
448
449     @param rootstrap_url: The url of the roostrap
450     @type rootstrap_url: C{string}
451     
452     @rtype: C{string} 
453     @return: 'PASS', 'FAIL' or 'ERROR'
454     """
455     result = "FAIL"
456     host = _get_testrun_host(log)
457     if host is not None:
458         try:
459             host.init_testrun(request,
460                               testrun_id,
461                               program,
462                               options,
463                               notify_list,
464                               test_packages,
465                               image_url,
466                               rootstrap_url)
467
468             host.register_ta_plugins()
469             host.register_results_plugins()
470             host.run()
471             if host.testrun_result() == "PASS":
472                 result = "PASS"
473         except:
474             log.exception("Error in testrun")
475             result = REQUEST_ERROR
476         finally:
477             host.publish_result_links()
478             host.cleanup()
479             log.debug("Root log handlers: %s" % str(log.root.handlers))
480             for handler in log.root.handlers:
481                 log.root.removeHandler(handler)
482     return result