blob: 1c8c3d3d35ac06e2e80c870cba35b2831b2e0340 [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
showardfb64e6a2009-04-22 21:01:18 +000058import getpass, optparse, os, pwd, re, socket, sys, textwrap, traceback
59import socket, urllib2
mblighbe630eb2008-08-01 16:41:48 +000060from autotest_lib.cli import rpc
61from autotest_lib.frontend.afe.json_rpc import proxy
62
63
64# Maps the AFE keys to printable names.
65KEYS_TO_NAMES_EN = {'hostname': 'Host',
66 'platform': 'Platform',
67 'status': 'Status',
68 'locked': 'Locked',
69 'locked_by': 'Locked by',
mblighe163b032008-10-18 14:30:27 +000070 'lock_time': 'Locked time',
mblighbe630eb2008-08-01 16:41:48 +000071 'labels': 'Labels',
72 'description': 'Description',
73 'hosts': 'Hosts',
74 'users': 'Users',
75 'id': 'Id',
76 'name': 'Name',
77 'invalid': 'Valid',
78 'login': 'Login',
79 'access_level': 'Access Level',
80 'job_id': 'Job Id',
81 'job_owner': 'Job Owner',
82 'job_name': 'Job Name',
83 'test_type': 'Test Type',
84 'test_class': 'Test Class',
85 'path': 'Path',
86 'owner': 'Owner',
87 'status_counts': 'Status Counts',
88 'hosts_status': 'Host Status',
mblighfca5ed12009-11-06 02:59:56 +000089 'hosts_selected_status': 'Hosts filtered by Status',
mblighbe630eb2008-08-01 16:41:48 +000090 'priority': 'Priority',
91 'control_type': 'Control Type',
92 'created_on': 'Created On',
93 'synch_type': 'Synch Type',
94 'control_file': 'Control File',
showard989f25d2008-10-01 11:38:11 +000095 'only_if_needed': 'Use only if needed',
mblighe163b032008-10-18 14:30:27 +000096 'protection': 'Protection',
showard21baa452008-10-21 00:08:39 +000097 'run_verify': 'Run verify',
98 'reboot_before': 'Pre-job reboot',
99 'reboot_after': 'Post-job reboot',
mbligh140a23c2008-10-29 16:55:21 +0000100 'experimental': 'Experimental',
mbligh8fadff32009-03-09 21:19:59 +0000101 'synch_count': 'Sync Count',
showardfb64e6a2009-04-22 21:01:18 +0000102 'max_number_of_machines': 'Max. hosts to use',
showarda1e74b32009-05-12 17:32:04 +0000103 'parse_failed_repair': 'Include failed repair results',
showard088b8262009-07-01 22:12:35 +0000104 'atomic_group.name': 'Atomic Group Name',
mblighbe630eb2008-08-01 16:41:48 +0000105 }
106
107# In the failure, tag that will replace the item.
108FAIL_TAG = '<XYZ>'
109
mbligh8c7b04c2009-03-25 18:01:56 +0000110# Global socket timeout: uploading kernels can take much,
111# much longer than the default
mblighbe630eb2008-08-01 16:41:48 +0000112UPLOAD_SOCKET_TIMEOUT = 60*30
113
114
115# Convertion functions to be called for printing,
116# e.g. to print True/False for booleans.
117def __convert_platform(field):
mbligh0887d402009-01-30 00:50:29 +0000118 if field is None:
mblighbe630eb2008-08-01 16:41:48 +0000119 return ""
mbligh0887d402009-01-30 00:50:29 +0000120 elif isinstance(field, int):
mblighbe630eb2008-08-01 16:41:48 +0000121 # Can be 0/1 for False/True
122 return str(bool(field))
123 else:
124 # Can be a platform name
125 return field
126
127
showard989f25d2008-10-01 11:38:11 +0000128def _int_2_bool_string(value):
129 return str(bool(value))
130
131KEYS_CONVERT = {'locked': _int_2_bool_string,
mblighbe630eb2008-08-01 16:41:48 +0000132 'invalid': lambda flag: str(bool(not flag)),
showard989f25d2008-10-01 11:38:11 +0000133 'only_if_needed': _int_2_bool_string,
mblighbe630eb2008-08-01 16:41:48 +0000134 'platform': __convert_platform,
135 'labels': lambda labels: ', '.join(labels)}
136
showard088b8262009-07-01 22:12:35 +0000137
138def _get_item_key(item, key):
139 """Allow for lookups in nested dictionaries using '.'s within a key."""
140 if key in item:
141 return item[key]
142 nested_item = item
143 for subkey in key.split('.'):
144 if not subkey:
145 raise ValueError('empty subkey in %r' % key)
146 try:
147 nested_item = nested_item[subkey]
148 except KeyError, e:
149 raise KeyError('%r - looking up key %r in %r' %
150 (e, key, nested_item))
151 else:
152 return nested_item
153
154
mblighbe630eb2008-08-01 16:41:48 +0000155class CliError(Exception):
156 pass
157
158
mbligh9deeefa2009-05-01 23:11:08 +0000159class item_parse_info(object):
160 def __init__(self, attribute_name, inline_option='',
161 filename_option='', use_leftover=False):
162 """Object keeping track of the parsing options that will
163 make up the content of the atest attribute:
164 atttribute_name: the atest attribute name to populate (label)
165 inline_option: the option containing the items (--label)
166 filename_option: the option containing the filename (--blist)
167 use_leftover: whether to add the leftover arguments or not."""
168 self.attribute_name = attribute_name
169 self.filename_option = filename_option
170 self.inline_option = inline_option
171 self.use_leftover = use_leftover
172
173
174 def get_values(self, options, leftover=[]):
175 """Returns the value for that attribute by accumualting all
176 the values found through the inline option, the parsing of the
177 file and the leftover"""
178 def __get_items(string, split_re='[\s,]\s*'):
179 return (item.strip() for item in re.split(split_re, string)
180 if item)
181
182 if self.use_leftover:
183 add_on = leftover
184 leftover = []
185 else:
186 add_on = []
187
188 # Start with the add_on
189 result = set()
190 for items in add_on:
191 # Don't split on space here because the add-on
192 # may have some spaces (like the job name)
193 result.update(__get_items(items, split_re='[,]'))
194
195 # Process the inline_option, if any
196 try:
197 items = getattr(options, self.inline_option)
198 result.update(__get_items(items))
199 except (AttributeError, TypeError):
200 pass
201
202 # Process the file list, if any and not empty
203 # The file can contain space and/or comma separated items
204 try:
205 flist = getattr(options, self.filename_option)
206 file_content = []
207 for line in open(flist).readlines():
208 file_content += __get_items(line)
209 if len(file_content) == 0:
210 raise CliError("Empty file %s" % flist)
211 result.update(file_content)
212 except (AttributeError, TypeError):
213 pass
214 except IOError:
215 raise CliError("Could not open file %s" % flist)
216
217 return list(result), leftover
218
219
mblighbe630eb2008-08-01 16:41:48 +0000220class atest(object):
221 """Common class for generic processing
222 Should only be instantiated by itself for usage
223 references, otherwise, the <topic> objects should
224 be used."""
showardfb64e6a2009-04-22 21:01:18 +0000225 msg_topic = "[acl|host|job|label|atomicgroup|test|user]"
mblighbe630eb2008-08-01 16:41:48 +0000226 usage_action = "[action]"
227 msg_items = ''
228
229 def invalid_arg(self, header, follow_up=''):
230 twrap = textwrap.TextWrapper(initial_indent=' ',
231 subsequent_indent=' ')
232 rest = twrap.fill(follow_up)
233
234 if self.kill_on_failure:
235 self.invalid_syntax(header + rest)
236 else:
237 print >> sys.stderr, header + rest
238
239
240 def invalid_syntax(self, msg):
241 print
242 print >> sys.stderr, msg
243 print
244 print "usage:",
245 print self._get_usage()
246 print
247 sys.exit(1)
248
249
250 def generic_error(self, msg):
showardfb64e6a2009-04-22 21:01:18 +0000251 if self.debug:
252 traceback.print_exc()
mblighbe630eb2008-08-01 16:41:48 +0000253 print >> sys.stderr, msg
254 sys.exit(1)
255
256
mbligh7a3ebe32008-12-01 17:10:33 +0000257 def parse_json_exception(self, full_error):
258 """Parses the JSON exception to extract the bad
259 items and returns them
260 This is very kludgy for the moment, but we would need
261 to refactor the exceptions sent from the front end
262 to make this better"""
263 errmsg = str(full_error).split('Traceback')[0].rstrip('\n')
264 parts = errmsg.split(':')
265 # Kludge: If there are 2 colons the last parts contains
266 # the items that failed.
267 if len(parts) != 3:
268 return []
269 return [item.strip() for item in parts[2].split(',') if item.strip()]
270
271
mblighb68405d2010-03-11 18:32:39 +0000272 def failure(self, full_error, item=None, what_failed='', fatal=False):
mblighbe630eb2008-08-01 16:41:48 +0000273 """If kill_on_failure, print this error and die,
274 otherwise, queue the error and accumulate all the items
275 that triggered the same error."""
276
277 if self.debug:
278 errmsg = str(full_error)
279 else:
280 errmsg = str(full_error).split('Traceback')[0].rstrip('\n')
281
mblighb68405d2010-03-11 18:32:39 +0000282 if self.kill_on_failure or fatal:
mblighbe630eb2008-08-01 16:41:48 +0000283 print >> sys.stderr, "%s\n %s" % (what_failed, errmsg)
284 sys.exit(1)
285
286 # Build a dictionary with the 'what_failed' as keys. The
287 # values are dictionaries with the errmsg as keys and a set
288 # of items as values.
mbligh1ef218d2009-08-03 16:57:56 +0000289 # self.failed =
mblighbe630eb2008-08-01 16:41:48 +0000290 # {'Operation delete_host_failed': {'AclAccessViolation:
291 # set('host0', 'host1')}}
292 # Try to gather all the same error messages together,
293 # even if they contain the 'item'
294 if item and item in errmsg:
295 errmsg = errmsg.replace(item, FAIL_TAG)
296 if self.failed.has_key(what_failed):
297 self.failed[what_failed].setdefault(errmsg, set()).add(item)
298 else:
299 self.failed[what_failed] = {errmsg: set([item])}
300
301
302 def show_all_failures(self):
303 if not self.failed:
304 return 0
305 for what_failed in self.failed.keys():
306 print >> sys.stderr, what_failed + ':'
307 for (errmsg, items) in self.failed[what_failed].iteritems():
308 if len(items) == 0:
309 print >> sys.stderr, errmsg
310 elif items == set(['']):
311 print >> sys.stderr, ' ' + errmsg
312 elif len(items) == 1:
313 # Restore the only item
314 if FAIL_TAG in errmsg:
315 errmsg = errmsg.replace(FAIL_TAG, items.pop())
316 else:
317 errmsg = '%s (%s)' % (errmsg, items.pop())
318 print >> sys.stderr, ' ' + errmsg
319 else:
320 print >> sys.stderr, ' ' + errmsg + ' with <XYZ> in:'
321 twrap = textwrap.TextWrapper(initial_indent=' ',
322 subsequent_indent=' ')
323 items = list(items)
324 items.sort()
325 print >> sys.stderr, twrap.fill(', '.join(items))
326 return 1
327
328
329 def __init__(self):
330 """Setup the parser common options"""
331 # Initialized for unit tests.
332 self.afe = None
333 self.failed = {}
334 self.data = {}
335 self.debug = False
mbligh47dc4d22009-02-12 21:48:34 +0000336 self.parse_delim = '|'
mblighbe630eb2008-08-01 16:41:48 +0000337 self.kill_on_failure = False
338 self.web_server = ''
339 self.verbose = False
mbligh9deeefa2009-05-01 23:11:08 +0000340 self.topic_parse_info = item_parse_info(attribute_name='not_used')
mblighbe630eb2008-08-01 16:41:48 +0000341
342 self.parser = optparse.OptionParser(self._get_usage())
343 self.parser.add_option('-g', '--debug',
344 help='Print debugging information',
345 action='store_true', default=False)
346 self.parser.add_option('--kill-on-failure',
347 help='Stop at the first failure',
348 action='store_true', default=False)
349 self.parser.add_option('--parse',
mbligh47dc4d22009-02-12 21:48:34 +0000350 help='Print the output using | '
mblighbe630eb2008-08-01 16:41:48 +0000351 'separated key=value fields',
352 action='store_true', default=False)
mbligh47dc4d22009-02-12 21:48:34 +0000353 self.parser.add_option('--parse-delim',
354 help='Delimiter to use to separate the '
355 'key=value fields', default='|')
mblighbe630eb2008-08-01 16:41:48 +0000356 self.parser.add_option('-v', '--verbose',
357 action='store_true', default=False)
358 self.parser.add_option('-w', '--web',
359 help='Specify the autotest server '
360 'to talk to',
361 action='store', type='string',
362 dest='web_server', default=None)
363
mblighbe630eb2008-08-01 16:41:48 +0000364
mblighbe630eb2008-08-01 16:41:48 +0000365 def _get_usage(self):
366 return "atest %s %s [options] %s" % (self.msg_topic.lower(),
367 self.usage_action,
368 self.msg_items)
369
370
mbligh5a496082009-08-03 16:44:54 +0000371 def backward_compatibility(self, action, argv):
372 """To be overidden by subclass if their syntax changed"""
373 return action
374
375
mbligh9deeefa2009-05-01 23:11:08 +0000376 def parse(self, parse_info=[], req_items=None):
377 """parse_info is a list of item_parse_info objects
mblighbe630eb2008-08-01 16:41:48 +0000378
mbligh9deeefa2009-05-01 23:11:08 +0000379 There should only be one use_leftover set to True in the list.
mblighbe630eb2008-08-01 16:41:48 +0000380
mbligh9deeefa2009-05-01 23:11:08 +0000381 Also check that the req_items is not empty after parsing."""
382 (options, leftover) = self.parse_global()
mblighbe630eb2008-08-01 16:41:48 +0000383
mbligh9deeefa2009-05-01 23:11:08 +0000384 all_parse_info = parse_info[:]
385 all_parse_info.append(self.topic_parse_info)
386
387 try:
388 for item_parse_info in all_parse_info:
389 values, leftover = item_parse_info.get_values(options,
390 leftover)
391 setattr(self, item_parse_info.attribute_name, values)
392 except CliError, s:
393 self.invalid_syntax(s)
mblighbe630eb2008-08-01 16:41:48 +0000394
395 if (req_items and not getattr(self, req_items, None)):
396 self.invalid_syntax('%s %s requires at least one %s' %
397 (self.msg_topic,
398 self.usage_action,
399 self.msg_topic))
400
401 return (options, leftover)
402
403
mbligh9deeefa2009-05-01 23:11:08 +0000404 def parse_global(self):
405 """Parse the global arguments.
mblighbe630eb2008-08-01 16:41:48 +0000406
407 It consumes what the common object needs to know, and
408 let the children look at all the options. We could
409 remove the options that we have used, but there is no
410 harm in leaving them, and the children may need them
411 in the future.
412
413 Must be called from its children parse()"""
414 (options, leftover) = self.parser.parse_args()
415 # Handle our own options setup in __init__()
416 self.debug = options.debug
417 self.kill_on_failure = options.kill_on_failure
418
419 if options.parse:
420 suffix = '_parse'
421 else:
422 suffix = '_std'
423 for func in ['print_fields', 'print_table',
mblighdf75f8b2008-11-18 19:07:42 +0000424 'print_by_ids', 'print_list']:
mblighbe630eb2008-08-01 16:41:48 +0000425 setattr(self, func, getattr(self, func + suffix))
426
mbligh47dc4d22009-02-12 21:48:34 +0000427 self.parse_delim = options.parse_delim
428
mblighbe630eb2008-08-01 16:41:48 +0000429 self.verbose = options.verbose
430 self.web_server = options.web_server
mblighb68405d2010-03-11 18:32:39 +0000431 try:
432 self.afe = rpc.afe_comm(self.web_server)
433 except rpc.AuthError, s:
434 self.failure(str(s), fatal=True)
mblighbe630eb2008-08-01 16:41:48 +0000435
436 return (options, leftover)
437
438
439 def check_and_create_items(self, op_get, op_create,
440 items, **data_create):
441 """Create the items if they don't exist already"""
442 for item in items:
443 ret = self.execute_rpc(op_get, name=item)
444
445 if len(ret) == 0:
446 try:
447 data_create['name'] = item
448 self.execute_rpc(op_create, **data_create)
449 except CliError:
450 continue
451
452
453 def execute_rpc(self, op, item='', **data):
454 retry = 2
455 while retry:
456 try:
457 return self.afe.run(op, **data)
458 except urllib2.URLError, err:
mbligh11efd232008-11-27 00:20:46 +0000459 if hasattr(err, 'reason'):
460 if 'timed out' not in err.reason:
461 self.invalid_syntax('Invalid server name %s: %s' %
462 (self.afe.web_server, err))
463 if hasattr(err, 'code'):
showard53d91e22010-01-15 00:18:27 +0000464 error_parts = [str(err)]
465 if self.debug:
466 error_parts.append(err.read()) # read the response body
467 self.failure('\n\n'.join(error_parts), item=item,
mbligh11efd232008-11-27 00:20:46 +0000468 what_failed=("Error received from web server"))
469 raise CliError("Error from web server")
mblighbe630eb2008-08-01 16:41:48 +0000470 if self.debug:
471 print 'retrying: %r %d' % (data, retry)
472 retry -= 1
473 if retry == 0:
474 if item:
475 myerr = '%s timed out for %s' % (op, item)
476 else:
477 myerr = '%s timed out' % op
478 self.failure(myerr, item=item,
479 what_failed=("Timed-out contacting "
480 "the Autotest server"))
481 raise CliError("Timed-out contacting the Autotest server")
482 except Exception, full_error:
483 # There are various exceptions throwns by JSON,
484 # urllib & httplib, so catch them all.
485 self.failure(full_error, item=item,
486 what_failed='Operation %s failed' % op)
487 raise CliError(str(full_error))
488
489
490 # There is no output() method in the atest object (yet?)
491 # but here are some helper functions to be used by its
492 # children
493 def print_wrapped(self, msg, values):
494 if len(values) == 0:
495 return
496 elif len(values) == 1:
497 print msg + ': '
498 elif len(values) > 1:
499 if msg.endswith('s'):
500 print msg + ': '
501 else:
502 print msg + 's: '
503
504 values.sort()
mbligh552d2402009-09-18 19:35:23 +0000505
506 if 'AUTOTEST_CLI_NO_WRAP' in os.environ:
507 print '\n'.join(values)
508 return
509
mblighbe630eb2008-08-01 16:41:48 +0000510 twrap = textwrap.TextWrapper(initial_indent='\t',
511 subsequent_indent='\t')
512 print twrap.fill(', '.join(values))
513
514
515 def __conv_value(self, type, value):
516 return KEYS_CONVERT.get(type, str)(value)
517
518
519 def print_fields_std(self, items, keys, title=None):
520 """Print the keys in each item, one on each line"""
521 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000522 return
523 if title:
524 print title
525 for item in items:
526 for key in keys:
527 print '%s: %s' % (KEYS_TO_NAMES_EN[key],
528 self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000529 _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000530
531
532 def print_fields_parse(self, items, keys, title=None):
533 """Print the keys in each item as comma
534 separated name=value"""
mblighbe630eb2008-08-01 16:41:48 +0000535 for item in items:
536 values = ['%s=%s' % (KEYS_TO_NAMES_EN[key],
537 self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000538 _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000539 for key in keys
540 if self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000541 _get_item_key(item, key)) != '']
mbligh47dc4d22009-02-12 21:48:34 +0000542 print self.parse_delim.join(values)
mblighbe630eb2008-08-01 16:41:48 +0000543
544
545 def __find_justified_fmt(self, items, keys):
546 """Find the max length for each field."""
547 lens = {}
548 # Don't justify the last field, otherwise we have blank
549 # lines when the max is overlaps but the current values
550 # are smaller
551 if not items:
552 print "No results"
553 return
554 for key in keys[:-1]:
555 lens[key] = max(len(self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000556 _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000557 for item in items)
558 lens[key] = max(lens[key], len(KEYS_TO_NAMES_EN[key]))
559 lens[keys[-1]] = 0
560
561 return ' '.join(["%%-%ds" % lens[key] for key in keys])
562
563
mbligh838c7472009-05-13 20:56:50 +0000564 def print_table_std(self, items, keys_header, sublist_keys=()):
mblighbe630eb2008-08-01 16:41:48 +0000565 """Print a mix of header and lists in a user readable
566 format
567 The headers are justified, the sublist_keys are wrapped."""
568 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000569 return
570 fmt = self.__find_justified_fmt(items, keys_header)
571 header = tuple(KEYS_TO_NAMES_EN[key] for key in keys_header)
572 print fmt % header
573 for item in items:
showard088b8262009-07-01 22:12:35 +0000574 values = tuple(self.__conv_value(key,
575 _get_item_key(item, key))
mblighbe630eb2008-08-01 16:41:48 +0000576 for key in keys_header)
577 print fmt % values
mbligh838c7472009-05-13 20:56:50 +0000578 if sublist_keys:
mblighbe630eb2008-08-01 16:41:48 +0000579 for key in sublist_keys:
580 self.print_wrapped(KEYS_TO_NAMES_EN[key],
showard088b8262009-07-01 22:12:35 +0000581 _get_item_key(item, key))
mblighbe630eb2008-08-01 16:41:48 +0000582 print '\n'
583
584
mbligh838c7472009-05-13 20:56:50 +0000585 def print_table_parse(self, items, keys_header, sublist_keys=()):
mblighbe630eb2008-08-01 16:41:48 +0000586 """Print a mix of header and lists in a user readable
587 format"""
mblighbe630eb2008-08-01 16:41:48 +0000588 for item in items:
589 values = ['%s=%s' % (KEYS_TO_NAMES_EN[key],
showard088b8262009-07-01 22:12:35 +0000590 self.__conv_value(key, _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000591 for key in keys_header
592 if self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000593 _get_item_key(item, key)) != '']
mblighbe630eb2008-08-01 16:41:48 +0000594
mbligh838c7472009-05-13 20:56:50 +0000595 if sublist_keys:
mblighbe630eb2008-08-01 16:41:48 +0000596 [values.append('%s=%s'% (KEYS_TO_NAMES_EN[key],
showard088b8262009-07-01 22:12:35 +0000597 ','.join(_get_item_key(item, key))))
mblighbe630eb2008-08-01 16:41:48 +0000598 for key in sublist_keys
showard088b8262009-07-01 22:12:35 +0000599 if len(_get_item_key(item, key))]
mblighbe630eb2008-08-01 16:41:48 +0000600
mbligh47dc4d22009-02-12 21:48:34 +0000601 print self.parse_delim.join(values)
mblighbe630eb2008-08-01 16:41:48 +0000602
603
604 def print_by_ids_std(self, items, title=None, line_before=False):
605 """Prints ID & names of items in a user readable form"""
606 if not items:
607 return
608 if line_before:
609 print
610 if title:
611 print title + ':'
612 self.print_table_std(items, keys_header=['id', 'name'])
613
614
615 def print_by_ids_parse(self, items, title=None, line_before=False):
616 """Prints ID & names of items in a parseable format"""
617 if not items:
mblighbe630eb2008-08-01 16:41:48 +0000618 return
619 if title:
620 print title + '=',
621 values = []
622 for item in items:
623 values += ['%s=%s' % (KEYS_TO_NAMES_EN[key],
624 self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000625 _get_item_key(item, key)))
mblighbe630eb2008-08-01 16:41:48 +0000626 for key in ['id', 'name']
627 if self.__conv_value(key,
showard088b8262009-07-01 22:12:35 +0000628 _get_item_key(item, key)) != '']
mbligh47dc4d22009-02-12 21:48:34 +0000629 print self.parse_delim.join(values)
mblighdf75f8b2008-11-18 19:07:42 +0000630
631
632 def print_list_std(self, items, key):
633 """Print a wrapped list of results"""
634 if not items:
mblighdf75f8b2008-11-18 19:07:42 +0000635 return
showard088b8262009-07-01 22:12:35 +0000636 print ' '.join(_get_item_key(item, key) for item in items)
mblighdf75f8b2008-11-18 19:07:42 +0000637
638
639 def print_list_parse(self, items, key):
640 """Print a wrapped list of results"""
641 if not items:
mblighdf75f8b2008-11-18 19:07:42 +0000642 return
643 print '%s=%s' % (KEYS_TO_NAMES_EN[key],
showard088b8262009-07-01 22:12:35 +0000644 ','.join(_get_item_key(item, key) for item in items))