blob: 4886350c3536893062997a7430f30c7d8170d3be [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
mbligh8c7b04c2009-03-25 18:01:56 +0000105# Global socket timeout: uploading kernels can take much,
106# much longer than the default
mblighbe630eb2008-08-01 16:41:48 +0000107UPLOAD_SOCKET_TIMEOUT = 60*30
108
109
110# Convertion functions to be called for printing,
111# e.g. to print True/False for booleans.
112def __convert_platform(field):
mbligh0887d402009-01-30 00:50:29 +0000113 if field is None:
mblighbe630eb2008-08-01 16:41:48 +0000114 return ""
mbligh0887d402009-01-30 00:50:29 +0000115 elif isinstance(field, int):
mblighbe630eb2008-08-01 16:41:48 +0000116 # Can be 0/1 for False/True
117 return str(bool(field))
118 else:
119 # Can be a platform name
120 return field
121
122
showard989f25d2008-10-01 11:38:11 +0000123def _int_2_bool_string(value):
124 return str(bool(value))
125
126KEYS_CONVERT = {'locked': _int_2_bool_string,
mblighbe630eb2008-08-01 16:41:48 +0000127 'invalid': lambda flag: str(bool(not flag)),
showard989f25d2008-10-01 11:38:11 +0000128 'only_if_needed': _int_2_bool_string,
mblighbe630eb2008-08-01 16:41:48 +0000129 'platform': __convert_platform,
130 'labels': lambda labels: ', '.join(labels)}
131
132class CliError(Exception):
133 pass
134
135
136class atest(object):
137 """Common class for generic processing
138 Should only be instantiated by itself for usage
139 references, otherwise, the <topic> objects should
140 be used."""
mbligh76dced82008-08-11 23:46:57 +0000141 msg_topic = "[acl|host|job|label|test|user]"
mblighbe630eb2008-08-01 16:41:48 +0000142 usage_action = "[action]"
143 msg_items = ''
144
145 def invalid_arg(self, header, follow_up=''):
146 twrap = textwrap.TextWrapper(initial_indent=' ',
147 subsequent_indent=' ')
148 rest = twrap.fill(follow_up)
149
150 if self.kill_on_failure:
151 self.invalid_syntax(header + rest)
152 else:
153 print >> sys.stderr, header + rest
154
155
156 def invalid_syntax(self, msg):
157 print
158 print >> sys.stderr, msg
159 print
160 print "usage:",
161 print self._get_usage()
162 print
163 sys.exit(1)
164
165
166 def generic_error(self, msg):
167 print >> sys.stderr, msg
168 sys.exit(1)
169
170
mbligh7a3ebe32008-12-01 17:10:33 +0000171 def parse_json_exception(self, full_error):
172 """Parses the JSON exception to extract the bad
173 items and returns them
174 This is very kludgy for the moment, but we would need
175 to refactor the exceptions sent from the front end
176 to make this better"""
177 errmsg = str(full_error).split('Traceback')[0].rstrip('\n')
178 parts = errmsg.split(':')
179 # Kludge: If there are 2 colons the last parts contains
180 # the items that failed.
181 if len(parts) != 3:
182 return []
183 return [item.strip() for item in parts[2].split(',') if item.strip()]
184
185
mblighbe630eb2008-08-01 16:41:48 +0000186 def failure(self, full_error, item=None, what_failed=''):
187 """If kill_on_failure, print this error and die,
188 otherwise, queue the error and accumulate all the items
189 that triggered the same error."""
190
191 if self.debug:
192 errmsg = str(full_error)
193 else:
194 errmsg = str(full_error).split('Traceback')[0].rstrip('\n')
195
196 if self.kill_on_failure:
197 print >> sys.stderr, "%s\n %s" % (what_failed, errmsg)
198 sys.exit(1)
199
200 # Build a dictionary with the 'what_failed' as keys. The
201 # values are dictionaries with the errmsg as keys and a set
202 # of items as values.
203 # self.failed =
204 # {'Operation delete_host_failed': {'AclAccessViolation:
205 # set('host0', 'host1')}}
206 # Try to gather all the same error messages together,
207 # even if they contain the 'item'
208 if item and item in errmsg:
209 errmsg = errmsg.replace(item, FAIL_TAG)
210 if self.failed.has_key(what_failed):
211 self.failed[what_failed].setdefault(errmsg, set()).add(item)
212 else:
213 self.failed[what_failed] = {errmsg: set([item])}
214
215
216 def show_all_failures(self):
217 if not self.failed:
218 return 0
219 for what_failed in self.failed.keys():
220 print >> sys.stderr, what_failed + ':'
221 for (errmsg, items) in self.failed[what_failed].iteritems():
222 if len(items) == 0:
223 print >> sys.stderr, errmsg
224 elif items == set(['']):
225 print >> sys.stderr, ' ' + errmsg
226 elif len(items) == 1:
227 # Restore the only item
228 if FAIL_TAG in errmsg:
229 errmsg = errmsg.replace(FAIL_TAG, items.pop())
230 else:
231 errmsg = '%s (%s)' % (errmsg, items.pop())
232 print >> sys.stderr, ' ' + errmsg
233 else:
234 print >> sys.stderr, ' ' + errmsg + ' with <XYZ> in:'
235 twrap = textwrap.TextWrapper(initial_indent=' ',
236 subsequent_indent=' ')
237 items = list(items)
238 items.sort()
239 print >> sys.stderr, twrap.fill(', '.join(items))
240 return 1
241
242
243 def __init__(self):
244 """Setup the parser common options"""
245 # Initialized for unit tests.
246 self.afe = None
247 self.failed = {}
248 self.data = {}
249 self.debug = False
mbligh47dc4d22009-02-12 21:48:34 +0000250 self.parse_delim = '|'
mblighbe630eb2008-08-01 16:41:48 +0000251 self.kill_on_failure = False
252 self.web_server = ''
253 self.verbose = False
254
255 self.parser = optparse.OptionParser(self._get_usage())
256 self.parser.add_option('-g', '--debug',
257 help='Print debugging information',
258 action='store_true', default=False)
259 self.parser.add_option('--kill-on-failure',
260 help='Stop at the first failure',
261 action='store_true', default=False)
262 self.parser.add_option('--parse',
mbligh47dc4d22009-02-12 21:48:34 +0000263 help='Print the output using | '
mblighbe630eb2008-08-01 16:41:48 +0000264 'separated key=value fields',
265 action='store_true', default=False)
mbligh47dc4d22009-02-12 21:48:34 +0000266 self.parser.add_option('--parse-delim',
267 help='Delimiter to use to separate the '
268 'key=value fields', default='|')
mblighbe630eb2008-08-01 16:41:48 +0000269 self.parser.add_option('-v', '--verbose',
270 action='store_true', default=False)
271 self.parser.add_option('-w', '--web',
272 help='Specify the autotest server '
273 'to talk to',
274 action='store', type='string',
275 dest='web_server', default=None)
276
mblighbe630eb2008-08-01 16:41:48 +0000277
278 def _file_list(self, options, opt_file='', opt_list='', add_on=[]):
279 """Returns a list containing the unique items from the
280 options.<opt_list>, from the file options.<opt_file>,
281 and from the space separated add_on strings.
282 The opt_list can be space or comma separated list.
283 Used for host, acls, labels... arguments"""
mblighf1733342008-09-04 16:45:46 +0000284 def __get_items(string, split_on='[\s,]\s*'):
285 return [item.strip() for item in re.split(split_on, string)
286 if item]
287
mblighbe630eb2008-08-01 16:41:48 +0000288 # Start with the add_on
289 result = set()
mblighf1733342008-09-04 16:45:46 +0000290 for items in add_on:
291 # Don't split on space here because the add-on
292 # may have some spaces (like the job name)
293 #result.update(item.strip() for item in items.split(',') if item)
294 result.update(__get_items(items, split_on='[,]'))
mblighbe630eb2008-08-01 16:41:48 +0000295
296 # Process the opt_list, if any
297 try:
mblighf1733342008-09-04 16:45:46 +0000298 items = getattr(options, opt_list)
299 result.update(__get_items(items))
mblighbe630eb2008-08-01 16:41:48 +0000300 except (AttributeError, TypeError):
301 pass
302
303 # Process the file list, if any and not empty
304 # The file can contain space and/or comma separated items
305 try:
306 flist = getattr(options, opt_file)
307 file_content = []
308 for line in open(flist).readlines():
mblighf1733342008-09-04 16:45:46 +0000309 file_content += __get_items(line)
mblighbe630eb2008-08-01 16:41:48 +0000310 if len(file_content) == 0:
311 self.invalid_syntax("Empty file %s" % flist)
mblighf1733342008-09-04 16:45:46 +0000312 result.update(file_content)
mblighbe630eb2008-08-01 16:41:48 +0000313 except (AttributeError, TypeError):
314 pass
315 except IOError:
316 self.invalid_syntax("Could not open file %s" % flist)
317
318 return list(result)
319
320
321 def _get_usage(self):
322 return "atest %s %s [options] %s" % (self.msg_topic.lower(),
323 self.usage_action,
324 self.msg_items)
325
326
327 def parse_with_flist(self, flists, req_items):
328 """Flists is a list of tuples containing:
329 (attribute, opt_fname, opt_list, use_leftover)
330
331 self.<atttribute> will be populated with a set
332 containing the lines of the file named
333 options.<opt_fname> and the options.<opt_list> values
334 and the leftover from the parsing if use_leftover is
335 True. There should only be one use_leftover set to
336 True in the list.
337 Also check if the req_items is not empty after parsing."""
338 (options, leftover) = atest.parse(self)
mblighd876f452008-12-03 15:09:17 +0000339 if leftover is None:
mblighbe630eb2008-08-01 16:41:48 +0000340 leftover = []
341
342 for (attribute, opt_fname, opt_list, use_leftover) in flists:
343 if use_leftover:
344 add_on = leftover
345 leftover = []
346 else:
347 add_on = []
348
349 setattr(self, attribute,
350 self._file_list(options,
351 opt_file=opt_fname,
352 opt_list=opt_list,
353 add_on=add_on))
354
355 if (req_items and not getattr(self, req_items, None)):
356 self.invalid_syntax('%s %s requires at least one %s' %
357 (self.msg_topic,
358 self.usage_action,
359 self.msg_topic))
360
361 return (options, leftover)
362
363
364 def parse(self):
365 """Parse all the arguments.
366
367 It consumes what the common object needs to know, and
368 let the children look at all the options. We could
369 remove the options that we have used, but there is no
370 harm in leaving them, and the children may need them
371 in the future.
372
373 Must be called from its children parse()"""
374 (options, leftover) = self.parser.parse_args()
375 # Handle our own options setup in __init__()
376 self.debug = options.debug
377 self.kill_on_failure = options.kill_on_failure
378
379 if options.parse:
380 suffix = '_parse'
381 else:
382 suffix = '_std'
383 for func in ['print_fields', 'print_table',
mblighdf75f8b2008-11-18 19:07:42 +0000384 'print_by_ids', 'print_list']:
mblighbe630eb2008-08-01 16:41:48 +0000385 setattr(self, func, getattr(self, func + suffix))
386
mbligh47dc4d22009-02-12 21:48:34 +0000387 self.parse_delim = options.parse_delim
388
mblighbe630eb2008-08-01 16:41:48 +0000389 self.verbose = options.verbose
390 self.web_server = options.web_server
391 self.afe = rpc.afe_comm(self.web_server)
392
393 return (options, leftover)
394
395
396 def check_and_create_items(self, op_get, op_create,
397 items, **data_create):
398 """Create the items if they don't exist already"""
399 for item in items:
400 ret = self.execute_rpc(op_get, name=item)
401
402 if len(ret) == 0:
403 try:
404 data_create['name'] = item
405 self.execute_rpc(op_create, **data_create)
406 except CliError:
407 continue
408
409
410 def execute_rpc(self, op, item='', **data):
411 retry = 2
412 while retry:
413 try:
414 return self.afe.run(op, **data)
415 except urllib2.URLError, err:
mbligh11efd232008-11-27 00:20:46 +0000416 if hasattr(err, 'reason'):
417 if 'timed out' not in err.reason:
418 self.invalid_syntax('Invalid server name %s: %s' %
419 (self.afe.web_server, err))
420 if hasattr(err, 'code'):
421 self.failure(str(err), item=item,
422 what_failed=("Error received from web server"))
423 raise CliError("Error from web server")
mblighbe630eb2008-08-01 16:41:48 +0000424 if self.debug:
425 print 'retrying: %r %d' % (data, retry)
426 retry -= 1
427 if retry == 0:
428 if item:
429 myerr = '%s timed out for %s' % (op, item)
430 else:
431 myerr = '%s timed out' % op
432 self.failure(myerr, item=item,
433 what_failed=("Timed-out contacting "
434 "the Autotest server"))
435 raise CliError("Timed-out contacting the Autotest server")
436 except Exception, full_error:
437 # There are various exceptions throwns by JSON,
438 # urllib & httplib, so catch them all.
439 self.failure(full_error, item=item,
440 what_failed='Operation %s failed' % op)
441 raise CliError(str(full_error))
442
443
444 # There is no output() method in the atest object (yet?)
445 # but here are some helper functions to be used by its
446 # children
447 def print_wrapped(self, msg, values):
448 if len(values) == 0:
449 return
450 elif len(values) == 1:
451 print msg + ': '
452 elif len(values) > 1:
453 if msg.endswith('s'):
454 print msg + ': '
455 else:
456 print msg + 's: '
457
458 values.sort()
459 twrap = textwrap.TextWrapper(initial_indent='\t',
460 subsequent_indent='\t')
461 print twrap.fill(', '.join(values))
462
463
464 def __conv_value(self, type, value):
465 return KEYS_CONVERT.get(type, str)(value)
466
467
468 def print_fields_std(self, items, keys, title=None):
469 """Print the keys in each item, one on each line"""
470 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000471 return
472 if title:
473 print title
474 for item in items:
475 for key in keys:
476 print '%s: %s' % (KEYS_TO_NAMES_EN[key],
477 self.__conv_value(key,
478 item[key]))
479
480
481 def print_fields_parse(self, items, keys, title=None):
482 """Print the keys in each item as comma
483 separated name=value"""
mblighbe630eb2008-08-01 16:41:48 +0000484 for item in items:
485 values = ['%s=%s' % (KEYS_TO_NAMES_EN[key],
486 self.__conv_value(key,
487 item[key]))
488 for key in keys
489 if self.__conv_value(key,
490 item[key]) != '']
mbligh47dc4d22009-02-12 21:48:34 +0000491 print self.parse_delim.join(values)
mblighbe630eb2008-08-01 16:41:48 +0000492
493
494 def __find_justified_fmt(self, items, keys):
495 """Find the max length for each field."""
496 lens = {}
497 # Don't justify the last field, otherwise we have blank
498 # lines when the max is overlaps but the current values
499 # are smaller
500 if not items:
501 print "No results"
502 return
503 for key in keys[:-1]:
504 lens[key] = max(len(self.__conv_value(key,
505 item[key]))
506 for item in items)
507 lens[key] = max(lens[key], len(KEYS_TO_NAMES_EN[key]))
508 lens[keys[-1]] = 0
509
510 return ' '.join(["%%-%ds" % lens[key] for key in keys])
511
512
513 def print_table_std(self, items, keys_header, sublist_keys={}):
514 """Print a mix of header and lists in a user readable
515 format
516 The headers are justified, the sublist_keys are wrapped."""
517 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000518 return
519 fmt = self.__find_justified_fmt(items, keys_header)
520 header = tuple(KEYS_TO_NAMES_EN[key] for key in keys_header)
521 print fmt % header
522 for item in items:
523 values = tuple(self.__conv_value(key, item[key])
524 for key in keys_header)
525 print fmt % values
526 if self.verbose and sublist_keys:
527 for key in sublist_keys:
528 self.print_wrapped(KEYS_TO_NAMES_EN[key],
529 item[key])
530 print '\n'
531
532
533 def print_table_parse(self, items, keys_header, sublist_keys=[]):
534 """Print a mix of header and lists in a user readable
535 format"""
mblighbe630eb2008-08-01 16:41:48 +0000536 for item in items:
537 values = ['%s=%s' % (KEYS_TO_NAMES_EN[key],
538 self.__conv_value(key, item[key]))
539 for key in keys_header
540 if self.__conv_value(key,
541 item[key]) != '']
542
543 if self.verbose:
544 [values.append('%s=%s'% (KEYS_TO_NAMES_EN[key],
545 ','.join(item[key])))
546 for key in sublist_keys
547 if len(item[key])]
548
mbligh47dc4d22009-02-12 21:48:34 +0000549 print self.parse_delim.join(values)
mblighbe630eb2008-08-01 16:41:48 +0000550
551
552 def print_by_ids_std(self, items, title=None, line_before=False):
553 """Prints ID & names of items in a user readable form"""
554 if not items:
555 return
556 if line_before:
557 print
558 if title:
559 print title + ':'
560 self.print_table_std(items, keys_header=['id', 'name'])
561
562
563 def print_by_ids_parse(self, items, title=None, line_before=False):
564 """Prints ID & names of items in a parseable format"""
565 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000566 return
567 if title:
568 print title + '=',
569 values = []
570 for item in items:
571 values += ['%s=%s' % (KEYS_TO_NAMES_EN[key],
572 self.__conv_value(key,
573 item[key]))
574 for key in ['id', 'name']
575 if self.__conv_value(key,
576 item[key]) != '']
mbligh47dc4d22009-02-12 21:48:34 +0000577 print self.parse_delim.join(values)
mblighdf75f8b2008-11-18 19:07:42 +0000578
579
580 def print_list_std(self, items, key):
581 """Print a wrapped list of results"""
582 if not items:
mblighdf75f8b2008-11-18 19:07:42 +0000583 return
584 print ' '.join(item[key] for item in items)
585
586
587 def print_list_parse(self, items, key):
588 """Print a wrapped list of results"""
589 if not items:
mblighdf75f8b2008-11-18 19:07:42 +0000590 return
591 print '%s=%s' % (KEYS_TO_NAMES_EN[key],
592 ','.join(item[key] for item in items))