blob: 6def2e4745a958884233c5955c860d4b8eeb04cb [file] [log] [blame]
mblighbe630eb2008-08-01 16:41:48 +00001#
2# Copyright 2008 Google Inc. All Rights Reserved.
3#
4"""
5This module contains the generic CLI object
6
7High Level Design:
8
9The atest class contains attributes & method generic to all the CLI
10operations.
11
12The class inheritance is shown here using the command
13'atest host create ...' as an example:
14
15atest <-- host <-- host_create <-- site_host_create
16
17Note: The site_<topic>.py and its classes are only needed if you need
18to override the common <topic>.py methods with your site specific ones.
19
20
21High Level Algorithm:
22
231. atest figures out the topic and action from the 2 first arguments
24 on the command line and imports the <topic> (or site_<topic>)
25 module.
26
271. Init
28 The main atest module creates a <topic>_<action> object. The
29 __init__() function is used to setup the parser options, if this
30 <action> has some specific options to add to its <topic>.
31
32 If it exists, the child __init__() method must call its parent
33 class __init__() before adding its own parser arguments.
34
352. Parsing
36 If the child wants to validate the parsing (e.g. make sure that
37 there are hosts in the arguments), or if it wants to check the
38 options it added in its __init__(), it should implement a parse()
39 method.
40
41 The child parser must call its parent parser and gets back the
42 options dictionary and the rest of the command line arguments
43 (leftover). Each level gets to see all the options, but the
44 leftovers can be deleted as they can be consumed by only one
45 object.
46
473. Execution
48 This execute() method is specific to the child and should use the
49 self.execute_rpc() to send commands to the Autotest Front-End. It
50 should return results.
51
524. Output
53 The child output() method is called with the execute() resutls as a
54 parameter. This is child-specific, but should leverage the
55 atest.print_*() methods.
56"""
57
58import os, sys, pwd, optparse, re, textwrap, urllib2, getpass, socket
59from autotest_lib.cli import rpc
60from autotest_lib.frontend.afe.json_rpc import proxy
61
62
63# Maps the AFE keys to printable names.
64KEYS_TO_NAMES_EN = {'hostname': 'Host',
65 'platform': 'Platform',
66 'status': 'Status',
67 'locked': 'Locked',
68 'locked_by': 'Locked by',
mblighe163b032008-10-18 14:30:27 +000069 'lock_time': 'Locked time',
mblighbe630eb2008-08-01 16:41:48 +000070 'labels': 'Labels',
71 'description': 'Description',
72 'hosts': 'Hosts',
73 'users': 'Users',
74 'id': 'Id',
75 'name': 'Name',
76 'invalid': 'Valid',
77 'login': 'Login',
78 'access_level': 'Access Level',
79 'job_id': 'Job Id',
80 'job_owner': 'Job Owner',
81 'job_name': 'Job Name',
82 'test_type': 'Test Type',
83 'test_class': 'Test Class',
84 'path': 'Path',
85 'owner': 'Owner',
86 'status_counts': 'Status Counts',
87 'hosts_status': 'Host Status',
88 'priority': 'Priority',
89 'control_type': 'Control Type',
90 'created_on': 'Created On',
91 'synch_type': 'Synch Type',
92 'control_file': 'Control File',
showard989f25d2008-10-01 11:38:11 +000093 'only_if_needed': 'Use only if needed',
mblighe163b032008-10-18 14:30:27 +000094 'protection': 'Protection',
showard21baa452008-10-21 00:08:39 +000095 'run_verify': 'Run verify',
96 'reboot_before': 'Pre-job reboot',
97 'reboot_after': 'Post-job reboot',
mbligh140a23c2008-10-29 16:55:21 +000098 'experimental': 'Experimental',
mbligh8fadff32009-03-09 21:19:59 +000099 'synch_count': 'Sync Count',
mblighbe630eb2008-08-01 16:41:48 +0000100 }
101
102# In the failure, tag that will replace the item.
103FAIL_TAG = '<XYZ>'
104
105# Global socket timeout
106DEFAULT_SOCKET_TIMEOUT = 5
107# For list commands, can take longer
108LIST_SOCKET_TIMEOUT = 30
109# For uploading kernels, can take much, much longer
110UPLOAD_SOCKET_TIMEOUT = 60*30
111
112
113# Convertion functions to be called for printing,
114# e.g. to print True/False for booleans.
115def __convert_platform(field):
mbligh0887d402009-01-30 00:50:29 +0000116 if field is None:
mblighbe630eb2008-08-01 16:41:48 +0000117 return ""
mbligh0887d402009-01-30 00:50:29 +0000118 elif isinstance(field, int):
mblighbe630eb2008-08-01 16:41:48 +0000119 # Can be 0/1 for False/True
120 return str(bool(field))
121 else:
122 # Can be a platform name
123 return field
124
125
showard989f25d2008-10-01 11:38:11 +0000126def _int_2_bool_string(value):
127 return str(bool(value))
128
129KEYS_CONVERT = {'locked': _int_2_bool_string,
mblighbe630eb2008-08-01 16:41:48 +0000130 'invalid': lambda flag: str(bool(not flag)),
showard989f25d2008-10-01 11:38:11 +0000131 'only_if_needed': _int_2_bool_string,
mblighbe630eb2008-08-01 16:41:48 +0000132 'platform': __convert_platform,
133 'labels': lambda labels: ', '.join(labels)}
134
135class CliError(Exception):
136 pass
137
138
139class atest(object):
140 """Common class for generic processing
141 Should only be instantiated by itself for usage
142 references, otherwise, the <topic> objects should
143 be used."""
mbligh76dced82008-08-11 23:46:57 +0000144 msg_topic = "[acl|host|job|label|test|user]"
mblighbe630eb2008-08-01 16:41:48 +0000145 usage_action = "[action]"
146 msg_items = ''
147
148 def invalid_arg(self, header, follow_up=''):
149 twrap = textwrap.TextWrapper(initial_indent=' ',
150 subsequent_indent=' ')
151 rest = twrap.fill(follow_up)
152
153 if self.kill_on_failure:
154 self.invalid_syntax(header + rest)
155 else:
156 print >> sys.stderr, header + rest
157
158
159 def invalid_syntax(self, msg):
160 print
161 print >> sys.stderr, msg
162 print
163 print "usage:",
164 print self._get_usage()
165 print
166 sys.exit(1)
167
168
169 def generic_error(self, msg):
170 print >> sys.stderr, msg
171 sys.exit(1)
172
173
mbligh7a3ebe32008-12-01 17:10:33 +0000174 def parse_json_exception(self, full_error):
175 """Parses the JSON exception to extract the bad
176 items and returns them
177 This is very kludgy for the moment, but we would need
178 to refactor the exceptions sent from the front end
179 to make this better"""
180 errmsg = str(full_error).split('Traceback')[0].rstrip('\n')
181 parts = errmsg.split(':')
182 # Kludge: If there are 2 colons the last parts contains
183 # the items that failed.
184 if len(parts) != 3:
185 return []
186 return [item.strip() for item in parts[2].split(',') if item.strip()]
187
188
mblighbe630eb2008-08-01 16:41:48 +0000189 def failure(self, full_error, item=None, what_failed=''):
190 """If kill_on_failure, print this error and die,
191 otherwise, queue the error and accumulate all the items
192 that triggered the same error."""
193
194 if self.debug:
195 errmsg = str(full_error)
196 else:
197 errmsg = str(full_error).split('Traceback')[0].rstrip('\n')
198
199 if self.kill_on_failure:
200 print >> sys.stderr, "%s\n %s" % (what_failed, errmsg)
201 sys.exit(1)
202
203 # Build a dictionary with the 'what_failed' as keys. The
204 # values are dictionaries with the errmsg as keys and a set
205 # of items as values.
206 # self.failed =
207 # {'Operation delete_host_failed': {'AclAccessViolation:
208 # set('host0', 'host1')}}
209 # Try to gather all the same error messages together,
210 # even if they contain the 'item'
211 if item and item in errmsg:
212 errmsg = errmsg.replace(item, FAIL_TAG)
213 if self.failed.has_key(what_failed):
214 self.failed[what_failed].setdefault(errmsg, set()).add(item)
215 else:
216 self.failed[what_failed] = {errmsg: set([item])}
217
218
219 def show_all_failures(self):
220 if not self.failed:
221 return 0
222 for what_failed in self.failed.keys():
223 print >> sys.stderr, what_failed + ':'
224 for (errmsg, items) in self.failed[what_failed].iteritems():
225 if len(items) == 0:
226 print >> sys.stderr, errmsg
227 elif items == set(['']):
228 print >> sys.stderr, ' ' + errmsg
229 elif len(items) == 1:
230 # Restore the only item
231 if FAIL_TAG in errmsg:
232 errmsg = errmsg.replace(FAIL_TAG, items.pop())
233 else:
234 errmsg = '%s (%s)' % (errmsg, items.pop())
235 print >> sys.stderr, ' ' + errmsg
236 else:
237 print >> sys.stderr, ' ' + errmsg + ' with <XYZ> in:'
238 twrap = textwrap.TextWrapper(initial_indent=' ',
239 subsequent_indent=' ')
240 items = list(items)
241 items.sort()
242 print >> sys.stderr, twrap.fill(', '.join(items))
243 return 1
244
245
246 def __init__(self):
247 """Setup the parser common options"""
248 # Initialized for unit tests.
249 self.afe = None
250 self.failed = {}
251 self.data = {}
252 self.debug = False
mbligh47dc4d22009-02-12 21:48:34 +0000253 self.parse_delim = '|'
mblighbe630eb2008-08-01 16:41:48 +0000254 self.kill_on_failure = False
255 self.web_server = ''
256 self.verbose = False
257
258 self.parser = optparse.OptionParser(self._get_usage())
259 self.parser.add_option('-g', '--debug',
260 help='Print debugging information',
261 action='store_true', default=False)
262 self.parser.add_option('--kill-on-failure',
263 help='Stop at the first failure',
264 action='store_true', default=False)
265 self.parser.add_option('--parse',
mbligh47dc4d22009-02-12 21:48:34 +0000266 help='Print the output using | '
mblighbe630eb2008-08-01 16:41:48 +0000267 'separated key=value fields',
268 action='store_true', default=False)
mbligh47dc4d22009-02-12 21:48:34 +0000269 self.parser.add_option('--parse-delim',
270 help='Delimiter to use to separate the '
271 'key=value fields', default='|')
mblighbe630eb2008-08-01 16:41:48 +0000272 self.parser.add_option('-v', '--verbose',
273 action='store_true', default=False)
274 self.parser.add_option('-w', '--web',
275 help='Specify the autotest server '
276 'to talk to',
277 action='store', type='string',
278 dest='web_server', default=None)
279
280 # Shorten the TCP timeout.
281 socket.setdefaulttimeout(DEFAULT_SOCKET_TIMEOUT)
282
283
284 def _file_list(self, options, opt_file='', opt_list='', add_on=[]):
285 """Returns a list containing the unique items from the
286 options.<opt_list>, from the file options.<opt_file>,
287 and from the space separated add_on strings.
288 The opt_list can be space or comma separated list.
289 Used for host, acls, labels... arguments"""
mblighf1733342008-09-04 16:45:46 +0000290 def __get_items(string, split_on='[\s,]\s*'):
291 return [item.strip() for item in re.split(split_on, string)
292 if item]
293
mblighbe630eb2008-08-01 16:41:48 +0000294 # Start with the add_on
295 result = set()
mblighf1733342008-09-04 16:45:46 +0000296 for items in add_on:
297 # Don't split on space here because the add-on
298 # may have some spaces (like the job name)
299 #result.update(item.strip() for item in items.split(',') if item)
300 result.update(__get_items(items, split_on='[,]'))
mblighbe630eb2008-08-01 16:41:48 +0000301
302 # Process the opt_list, if any
303 try:
mblighf1733342008-09-04 16:45:46 +0000304 items = getattr(options, opt_list)
305 result.update(__get_items(items))
mblighbe630eb2008-08-01 16:41:48 +0000306 except (AttributeError, TypeError):
307 pass
308
309 # Process the file list, if any and not empty
310 # The file can contain space and/or comma separated items
311 try:
312 flist = getattr(options, opt_file)
313 file_content = []
314 for line in open(flist).readlines():
mblighf1733342008-09-04 16:45:46 +0000315 file_content += __get_items(line)
mblighbe630eb2008-08-01 16:41:48 +0000316 if len(file_content) == 0:
317 self.invalid_syntax("Empty file %s" % flist)
mblighf1733342008-09-04 16:45:46 +0000318 result.update(file_content)
mblighbe630eb2008-08-01 16:41:48 +0000319 except (AttributeError, TypeError):
320 pass
321 except IOError:
322 self.invalid_syntax("Could not open file %s" % flist)
323
324 return list(result)
325
326
327 def _get_usage(self):
328 return "atest %s %s [options] %s" % (self.msg_topic.lower(),
329 self.usage_action,
330 self.msg_items)
331
332
333 def parse_with_flist(self, flists, req_items):
334 """Flists is a list of tuples containing:
335 (attribute, opt_fname, opt_list, use_leftover)
336
337 self.<atttribute> will be populated with a set
338 containing the lines of the file named
339 options.<opt_fname> and the options.<opt_list> values
340 and the leftover from the parsing if use_leftover is
341 True. There should only be one use_leftover set to
342 True in the list.
343 Also check if the req_items is not empty after parsing."""
344 (options, leftover) = atest.parse(self)
mblighd876f452008-12-03 15:09:17 +0000345 if leftover is None:
mblighbe630eb2008-08-01 16:41:48 +0000346 leftover = []
347
348 for (attribute, opt_fname, opt_list, use_leftover) in flists:
349 if use_leftover:
350 add_on = leftover
351 leftover = []
352 else:
353 add_on = []
354
355 setattr(self, attribute,
356 self._file_list(options,
357 opt_file=opt_fname,
358 opt_list=opt_list,
359 add_on=add_on))
360
361 if (req_items and not getattr(self, req_items, None)):
362 self.invalid_syntax('%s %s requires at least one %s' %
363 (self.msg_topic,
364 self.usage_action,
365 self.msg_topic))
366
367 return (options, leftover)
368
369
370 def parse(self):
371 """Parse all the arguments.
372
373 It consumes what the common object needs to know, and
374 let the children look at all the options. We could
375 remove the options that we have used, but there is no
376 harm in leaving them, and the children may need them
377 in the future.
378
379 Must be called from its children parse()"""
380 (options, leftover) = self.parser.parse_args()
381 # Handle our own options setup in __init__()
382 self.debug = options.debug
383 self.kill_on_failure = options.kill_on_failure
384
385 if options.parse:
386 suffix = '_parse'
387 else:
388 suffix = '_std'
389 for func in ['print_fields', 'print_table',
mblighdf75f8b2008-11-18 19:07:42 +0000390 'print_by_ids', 'print_list']:
mblighbe630eb2008-08-01 16:41:48 +0000391 setattr(self, func, getattr(self, func + suffix))
392
mbligh47dc4d22009-02-12 21:48:34 +0000393 self.parse_delim = options.parse_delim
394
mblighbe630eb2008-08-01 16:41:48 +0000395 self.verbose = options.verbose
396 self.web_server = options.web_server
397 self.afe = rpc.afe_comm(self.web_server)
398
399 return (options, leftover)
400
401
402 def check_and_create_items(self, op_get, op_create,
403 items, **data_create):
404 """Create the items if they don't exist already"""
405 for item in items:
406 ret = self.execute_rpc(op_get, name=item)
407
408 if len(ret) == 0:
409 try:
410 data_create['name'] = item
411 self.execute_rpc(op_create, **data_create)
412 except CliError:
413 continue
414
415
416 def execute_rpc(self, op, item='', **data):
417 retry = 2
418 while retry:
419 try:
420 return self.afe.run(op, **data)
421 except urllib2.URLError, err:
mbligh11efd232008-11-27 00:20:46 +0000422 if hasattr(err, 'reason'):
423 if 'timed out' not in err.reason:
424 self.invalid_syntax('Invalid server name %s: %s' %
425 (self.afe.web_server, err))
426 if hasattr(err, 'code'):
427 self.failure(str(err), item=item,
428 what_failed=("Error received from web server"))
429 raise CliError("Error from web server")
mblighbe630eb2008-08-01 16:41:48 +0000430 if self.debug:
431 print 'retrying: %r %d' % (data, retry)
432 retry -= 1
433 if retry == 0:
434 if item:
435 myerr = '%s timed out for %s' % (op, item)
436 else:
437 myerr = '%s timed out' % op
438 self.failure(myerr, item=item,
439 what_failed=("Timed-out contacting "
440 "the Autotest server"))
441 raise CliError("Timed-out contacting the Autotest server")
442 except Exception, full_error:
443 # There are various exceptions throwns by JSON,
444 # urllib & httplib, so catch them all.
445 self.failure(full_error, item=item,
446 what_failed='Operation %s failed' % op)
447 raise CliError(str(full_error))
448
449
450 # There is no output() method in the atest object (yet?)
451 # but here are some helper functions to be used by its
452 # children
453 def print_wrapped(self, msg, values):
454 if len(values) == 0:
455 return
456 elif len(values) == 1:
457 print msg + ': '
458 elif len(values) > 1:
459 if msg.endswith('s'):
460 print msg + ': '
461 else:
462 print msg + 's: '
463
464 values.sort()
465 twrap = textwrap.TextWrapper(initial_indent='\t',
466 subsequent_indent='\t')
467 print twrap.fill(', '.join(values))
468
469
470 def __conv_value(self, type, value):
471 return KEYS_CONVERT.get(type, str)(value)
472
473
474 def print_fields_std(self, items, keys, title=None):
475 """Print the keys in each item, one on each line"""
476 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000477 return
478 if title:
479 print title
480 for item in items:
481 for key in keys:
482 print '%s: %s' % (KEYS_TO_NAMES_EN[key],
483 self.__conv_value(key,
484 item[key]))
485
486
487 def print_fields_parse(self, items, keys, title=None):
488 """Print the keys in each item as comma
489 separated name=value"""
mblighbe630eb2008-08-01 16:41:48 +0000490 for item in items:
491 values = ['%s=%s' % (KEYS_TO_NAMES_EN[key],
492 self.__conv_value(key,
493 item[key]))
494 for key in keys
495 if self.__conv_value(key,
496 item[key]) != '']
mbligh47dc4d22009-02-12 21:48:34 +0000497 print self.parse_delim.join(values)
mblighbe630eb2008-08-01 16:41:48 +0000498
499
500 def __find_justified_fmt(self, items, keys):
501 """Find the max length for each field."""
502 lens = {}
503 # Don't justify the last field, otherwise we have blank
504 # lines when the max is overlaps but the current values
505 # are smaller
506 if not items:
507 print "No results"
508 return
509 for key in keys[:-1]:
510 lens[key] = max(len(self.__conv_value(key,
511 item[key]))
512 for item in items)
513 lens[key] = max(lens[key], len(KEYS_TO_NAMES_EN[key]))
514 lens[keys[-1]] = 0
515
516 return ' '.join(["%%-%ds" % lens[key] for key in keys])
517
518
519 def print_table_std(self, items, keys_header, sublist_keys={}):
520 """Print a mix of header and lists in a user readable
521 format
522 The headers are justified, the sublist_keys are wrapped."""
523 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000524 return
525 fmt = self.__find_justified_fmt(items, keys_header)
526 header = tuple(KEYS_TO_NAMES_EN[key] for key in keys_header)
527 print fmt % header
528 for item in items:
529 values = tuple(self.__conv_value(key, item[key])
530 for key in keys_header)
531 print fmt % values
532 if self.verbose and sublist_keys:
533 for key in sublist_keys:
534 self.print_wrapped(KEYS_TO_NAMES_EN[key],
535 item[key])
536 print '\n'
537
538
539 def print_table_parse(self, items, keys_header, sublist_keys=[]):
540 """Print a mix of header and lists in a user readable
541 format"""
mblighbe630eb2008-08-01 16:41:48 +0000542 for item in items:
543 values = ['%s=%s' % (KEYS_TO_NAMES_EN[key],
544 self.__conv_value(key, item[key]))
545 for key in keys_header
546 if self.__conv_value(key,
547 item[key]) != '']
548
549 if self.verbose:
550 [values.append('%s=%s'% (KEYS_TO_NAMES_EN[key],
551 ','.join(item[key])))
552 for key in sublist_keys
553 if len(item[key])]
554
mbligh47dc4d22009-02-12 21:48:34 +0000555 print self.parse_delim.join(values)
mblighbe630eb2008-08-01 16:41:48 +0000556
557
558 def print_by_ids_std(self, items, title=None, line_before=False):
559 """Prints ID & names of items in a user readable form"""
560 if not items:
561 return
562 if line_before:
563 print
564 if title:
565 print title + ':'
566 self.print_table_std(items, keys_header=['id', 'name'])
567
568
569 def print_by_ids_parse(self, items, title=None, line_before=False):
570 """Prints ID & names of items in a parseable format"""
571 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000572 return
573 if title:
574 print title + '=',
575 values = []
576 for item in items:
577 values += ['%s=%s' % (KEYS_TO_NAMES_EN[key],
578 self.__conv_value(key,
579 item[key]))
580 for key in ['id', 'name']
581 if self.__conv_value(key,
582 item[key]) != '']
mbligh47dc4d22009-02-12 21:48:34 +0000583 print self.parse_delim.join(values)
mblighdf75f8b2008-11-18 19:07:42 +0000584
585
586 def print_list_std(self, items, key):
587 """Print a wrapped list of results"""
588 if not items:
mblighdf75f8b2008-11-18 19:07:42 +0000589 return
590 print ' '.join(item[key] for item in items)
591
592
593 def print_list_parse(self, items, key):
594 """Print a wrapped list of results"""
595 if not items:
mblighdf75f8b2008-11-18 19:07:42 +0000596 return
597 print '%s=%s' % (KEYS_TO_NAMES_EN[key],
598 ','.join(item[key] for item in items))