grow-disk: wait until resync is completed
[ganeti-local] / lib / cli.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Module dealing with command line parsing"""
23
24
25 import sys
26 import textwrap
27 import os.path
28 import copy
29 import time
30 from cStringIO import StringIO
31
32 from ganeti import utils
33 from ganeti import logger
34 from ganeti import errors
35 from ganeti import constants
36 from ganeti import opcodes
37 from ganeti import luxi
38 from ganeti import ssconf
39
40 from optparse import (OptionParser, make_option, TitledHelpFormatter,
41                       Option, OptionValueError)
42
43 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
44            "SubmitOpCode", "GetClient",
45            "cli_option", "ikv_option", "keyval_option",
46            "GenerateTable", "AskUser",
47            "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
48            "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT", "SUBMIT_OPT",
49            "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
50            "FormatError", "SplitNodeOption", "SubmitOrSend",
51            "JobSubmittedException", "FormatTimestamp", "ParseTimespec",
52            "ValidateBeParams",
53            ]
54
55
56 def _ExtractTagsObject(opts, args):
57   """Extract the tag type object.
58
59   Note that this function will modify its args parameter.
60
61   """
62   if not hasattr(opts, "tag_type"):
63     raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
64   kind = opts.tag_type
65   if kind == constants.TAG_CLUSTER:
66     retval = kind, kind
67   elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
68     if not args:
69       raise errors.OpPrereqError("no arguments passed to the command")
70     name = args.pop(0)
71     retval = kind, name
72   else:
73     raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
74   return retval
75
76
77 def _ExtendTags(opts, args):
78   """Extend the args if a source file has been given.
79
80   This function will extend the tags with the contents of the file
81   passed in the 'tags_source' attribute of the opts parameter. A file
82   named '-' will be replaced by stdin.
83
84   """
85   fname = opts.tags_source
86   if fname is None:
87     return
88   if fname == "-":
89     new_fh = sys.stdin
90   else:
91     new_fh = open(fname, "r")
92   new_data = []
93   try:
94     # we don't use the nice 'new_data = [line.strip() for line in fh]'
95     # because of python bug 1633941
96     while True:
97       line = new_fh.readline()
98       if not line:
99         break
100       new_data.append(line.strip())
101   finally:
102     new_fh.close()
103   args.extend(new_data)
104
105
106 def ListTags(opts, args):
107   """List the tags on a given object.
108
109   This is a generic implementation that knows how to deal with all
110   three cases of tag objects (cluster, node, instance). The opts
111   argument is expected to contain a tag_type field denoting what
112   object type we work on.
113
114   """
115   kind, name = _ExtractTagsObject(opts, args)
116   op = opcodes.OpGetTags(kind=kind, name=name)
117   result = SubmitOpCode(op)
118   result = list(result)
119   result.sort()
120   for tag in result:
121     print tag
122
123
124 def AddTags(opts, args):
125   """Add tags on a given object.
126
127   This is a generic implementation that knows how to deal with all
128   three cases of tag objects (cluster, node, instance). The opts
129   argument is expected to contain a tag_type field denoting what
130   object type we work on.
131
132   """
133   kind, name = _ExtractTagsObject(opts, args)
134   _ExtendTags(opts, args)
135   if not args:
136     raise errors.OpPrereqError("No tags to be added")
137   op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
138   SubmitOpCode(op)
139
140
141 def RemoveTags(opts, args):
142   """Remove tags from a given object.
143
144   This is a generic implementation that knows how to deal with all
145   three cases of tag objects (cluster, node, instance). The opts
146   argument is expected to contain a tag_type field denoting what
147   object type we work on.
148
149   """
150   kind, name = _ExtractTagsObject(opts, args)
151   _ExtendTags(opts, args)
152   if not args:
153     raise errors.OpPrereqError("No tags to be removed")
154   op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
155   SubmitOpCode(op)
156
157
158 DEBUG_OPT = make_option("-d", "--debug", default=False,
159                         action="store_true",
160                         help="Turn debugging on")
161
162 NOHDR_OPT = make_option("--no-headers", default=False,
163                         action="store_true", dest="no_headers",
164                         help="Don't display column headers")
165
166 SEP_OPT = make_option("--separator", default=None,
167                       action="store", dest="separator",
168                       help="Separator between output fields"
169                       " (defaults to one space)")
170
171 USEUNITS_OPT = make_option("--human-readable", default=False,
172                            action="store_true", dest="human_readable",
173                            help="Print sizes in human readable format")
174
175 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
176                          type="string", help="Comma separated list of"
177                          " output fields",
178                          metavar="FIELDS")
179
180 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
181                         default=False, help="Force the operation")
182
183 TAG_SRC_OPT = make_option("--from", dest="tags_source",
184                           default=None, help="File with tag names")
185
186 SUBMIT_OPT = make_option("--submit", dest="submit_only",
187                          default=False, action="store_true",
188                          help="Submit the job and return the job ID, but"
189                          " don't wait for the job to finish")
190
191
192 def ARGS_FIXED(val):
193   """Macro-like function denoting a fixed number of arguments"""
194   return -val
195
196
197 def ARGS_ATLEAST(val):
198   """Macro-like function denoting a minimum number of arguments"""
199   return val
200
201
202 ARGS_NONE = None
203 ARGS_ONE = ARGS_FIXED(1)
204 ARGS_ANY = ARGS_ATLEAST(0)
205
206
207 def check_unit(option, opt, value):
208   """OptParsers custom converter for units.
209
210   """
211   try:
212     return utils.ParseUnit(value)
213   except errors.UnitParseError, err:
214     raise OptionValueError("option %s: %s" % (opt, err))
215
216
217 class CliOption(Option):
218   """Custom option class for optparse.
219
220   """
221   TYPES = Option.TYPES + ("unit",)
222   TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
223   TYPE_CHECKER["unit"] = check_unit
224
225
226 def _SplitKeyVal(opt, data):
227   """Convert a KeyVal string into a dict.
228
229   This function will convert a key=val[,...] string into a dict. Empty
230   values will be converted specially: keys which have the prefix 'no_'
231   will have the value=False and the prefix stripped, the others will
232   have value=True.
233
234   @type opt: string
235   @param opt: a string holding the option name for which we process the
236       data, used in building error messages
237   @type data: string
238   @param data: a string of the format key=val,key=val,...
239   @rtype: dict
240   @return: {key=val, key=val}
241   @raises errors.ParameterError: if there are duplicate keys
242
243   """
244   NO_PREFIX = "no_"
245   UN_PREFIX = "-"
246   kv_dict = {}
247   for elem in data.split(","):
248     if "=" in elem:
249       key, val = elem.split("=", 1)
250     else:
251       if elem.startswith(NO_PREFIX):
252         key, val = elem[len(NO_PREFIX):], False
253       elif elem.startswith(UN_PREFIX):
254         key, val = elem[len(UN_PREFIX):], None
255       else:
256         key, val = elem, True
257     if key in kv_dict:
258       raise errors.ParameterError("Duplicate key '%s' in option %s" %
259                                   (key, opt))
260     kv_dict[key] = val
261   return kv_dict
262
263
264 def check_ident_key_val(option, opt, value):
265   """Custom parser for the IdentKeyVal option type.
266
267   """
268   if ":" not in value:
269     retval =  (value, {})
270   else:
271     ident, rest = value.split(":", 1)
272     kv_dict = _SplitKeyVal(opt, rest)
273     retval = (ident, kv_dict)
274   return retval
275
276
277 class IdentKeyValOption(Option):
278   """Custom option class for ident:key=val,key=val options.
279
280   This will store the parsed values as a tuple (ident, {key: val}). As
281   such, multiple uses of this option via action=append is possible.
282
283   """
284   TYPES = Option.TYPES + ("identkeyval",)
285   TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
286   TYPE_CHECKER["identkeyval"] = check_ident_key_val
287
288
289 def check_key_val(option, opt, value):
290   """Custom parser for the KeyVal option type.
291
292   """
293   return _SplitKeyVal(opt, value)
294
295
296 class KeyValOption(Option):
297   """Custom option class for key=val,key=val options.
298
299   This will store the parsed values as a dict {key: val}.
300
301   """
302   TYPES = Option.TYPES + ("keyval",)
303   TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
304   TYPE_CHECKER["keyval"] = check_key_val
305
306
307 # optparse.py sets make_option, so we do it for our own option class, too
308 cli_option = CliOption
309 ikv_option = IdentKeyValOption
310 keyval_option = KeyValOption
311
312
313 def _ParseArgs(argv, commands, aliases):
314   """Parses the command line and return the function which must be
315   executed together with its arguments
316
317   Arguments:
318     argv: the command line
319
320     commands: dictionary with special contents, see the design doc for
321     cmdline handling
322     aliases: dictionary with command aliases {'alias': 'target, ...}
323
324   """
325   if len(argv) == 0:
326     binary = "<command>"
327   else:
328     binary = argv[0].split("/")[-1]
329
330   if len(argv) > 1 and argv[1] == "--version":
331     print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
332     # Quit right away. That way we don't have to care about this special
333     # argument. optparse.py does it the same.
334     sys.exit(0)
335
336   if len(argv) < 2 or not (argv[1] in commands or
337                            argv[1] in aliases):
338     # let's do a nice thing
339     sortedcmds = commands.keys()
340     sortedcmds.sort()
341     print ("Usage: %(bin)s {command} [options...] [argument...]"
342            "\n%(bin)s <command> --help to see details, or"
343            " man %(bin)s\n" % {"bin": binary})
344     # compute the max line length for cmd + usage
345     mlen = max([len(" %s" % cmd) for cmd in commands])
346     mlen = min(60, mlen) # should not get here...
347     # and format a nice command list
348     print "Commands:"
349     for cmd in sortedcmds:
350       cmdstr = " %s" % (cmd,)
351       help_text = commands[cmd][4]
352       help_lines = textwrap.wrap(help_text, 79-3-mlen)
353       print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
354       for line in help_lines:
355         print "%-*s   %s" % (mlen, "", line)
356     print
357     return None, None, None
358
359   # get command, unalias it, and look it up in commands
360   cmd = argv.pop(1)
361   if cmd in aliases:
362     if cmd in commands:
363       raise errors.ProgrammerError("Alias '%s' overrides an existing"
364                                    " command" % cmd)
365
366     if aliases[cmd] not in commands:
367       raise errors.ProgrammerError("Alias '%s' maps to non-existing"
368                                    " command '%s'" % (cmd, aliases[cmd]))
369
370     cmd = aliases[cmd]
371
372   func, nargs, parser_opts, usage, description = commands[cmd]
373   parser = OptionParser(option_list=parser_opts,
374                         description=description,
375                         formatter=TitledHelpFormatter(),
376                         usage="%%prog %s %s" % (cmd, usage))
377   parser.disable_interspersed_args()
378   options, args = parser.parse_args()
379   if nargs is None:
380     if len(args) != 0:
381       print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
382       return None, None, None
383   elif nargs < 0 and len(args) != -nargs:
384     print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
385                          (cmd, -nargs))
386     return None, None, None
387   elif nargs >= 0 and len(args) < nargs:
388     print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
389                          (cmd, nargs))
390     return None, None, None
391
392   return func, options, args
393
394
395 def SplitNodeOption(value):
396   """Splits the value of a --node option.
397
398   """
399   if value and ':' in value:
400     return value.split(':', 1)
401   else:
402     return (value, None)
403
404
405 def ValidateBeParams(bep):
406   """Parse and check the given beparams.
407
408   The function will update in-place the given dictionary.
409
410   @type bep: dict
411   @param bep: input beparams
412   @raise errors.ParameterError: if the input values are not OK
413   @raise errors.UnitParseError: if the input values are not OK
414
415   """
416   if constants.BE_MEMORY in bep:
417     bep[constants.BE_MEMORY] = utils.ParseUnit(bep[constants.BE_MEMORY])
418
419   if constants.BE_VCPUS in bep:
420     try:
421       bep[constants.BE_VCPUS] = int(bep[constants.BE_VCPUS])
422     except ValueError:
423       raise errors.ParameterError("Invalid number of VCPUs")
424
425
426 def AskUser(text, choices=None):
427   """Ask the user a question.
428
429   Args:
430     text - the question to ask.
431
432     choices - list with elements tuples (input_char, return_value,
433     description); if not given, it will default to: [('y', True,
434     'Perform the operation'), ('n', False, 'Do no do the operation')];
435     note that the '?' char is reserved for help
436
437   Returns: one of the return values from the choices list; if input is
438   not possible (i.e. not running with a tty, we return the last entry
439   from the list
440
441   """
442   if choices is None:
443     choices = [('y', True, 'Perform the operation'),
444                ('n', False, 'Do not perform the operation')]
445   if not choices or not isinstance(choices, list):
446     raise errors.ProgrammerError("Invalid choiches argument to AskUser")
447   for entry in choices:
448     if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
449       raise errors.ProgrammerError("Invalid choiches element to AskUser")
450
451   answer = choices[-1][1]
452   new_text = []
453   for line in text.splitlines():
454     new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
455   text = "\n".join(new_text)
456   try:
457     f = file("/dev/tty", "a+")
458   except IOError:
459     return answer
460   try:
461     chars = [entry[0] for entry in choices]
462     chars[-1] = "[%s]" % chars[-1]
463     chars.append('?')
464     maps = dict([(entry[0], entry[1]) for entry in choices])
465     while True:
466       f.write(text)
467       f.write('\n')
468       f.write("/".join(chars))
469       f.write(": ")
470       line = f.readline(2).strip().lower()
471       if line in maps:
472         answer = maps[line]
473         break
474       elif line == '?':
475         for entry in choices:
476           f.write(" %s - %s\n" % (entry[0], entry[2]))
477         f.write("\n")
478         continue
479   finally:
480     f.close()
481   return answer
482
483
484 class JobSubmittedException(Exception):
485   """Job was submitted, client should exit.
486
487   This exception has one argument, the ID of the job that was
488   submitted. The handler should print this ID.
489
490   This is not an error, just a structured way to exit from clients.
491
492   """
493
494
495 def SendJob(ops, cl=None):
496   """Function to submit an opcode without waiting for the results.
497
498   @type ops: list
499   @param ops: list of opcodes
500   @type cl: luxi.Client
501   @param cl: the luxi client to use for communicating with the master;
502              if None, a new client will be created
503
504   """
505   if cl is None:
506     cl = GetClient()
507
508   job_id = cl.SubmitJob(ops)
509
510   return job_id
511
512
513 def PollJob(job_id, cl=None, feedback_fn=None):
514   """Function to poll for the result of a job.
515
516   @type job_id: job identified
517   @param job_id: the job to poll for results
518   @type cl: luxi.Client
519   @param cl: the luxi client to use for communicating with the master;
520              if None, a new client will be created
521
522   """
523   if cl is None:
524     cl = GetClient()
525
526   prev_job_info = None
527   prev_logmsg_serial = None
528
529   while True:
530     result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
531                                  prev_logmsg_serial)
532     if not result:
533       # job not found, go away!
534       raise errors.JobLost("Job with id %s lost" % job_id)
535
536     # Split result, a tuple of (field values, log entries)
537     (job_info, log_entries) = result
538     (status, ) = job_info
539
540     if log_entries:
541       for log_entry in log_entries:
542         (serial, timestamp, _, message) = log_entry
543         if callable(feedback_fn):
544           feedback_fn(log_entry[1:])
545         else:
546           print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), message)
547         prev_logmsg_serial = max(prev_logmsg_serial, serial)
548
549     # TODO: Handle canceled and archived jobs
550     elif status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
551       break
552
553     prev_job_info = job_info
554
555   jobs = cl.QueryJobs([job_id], ["status", "opresult"])
556   if not jobs:
557     raise errors.JobLost("Job with id %s lost" % job_id)
558
559   status, result = jobs[0]
560   if status == constants.JOB_STATUS_SUCCESS:
561     return result
562   else:
563     raise errors.OpExecError(result)
564
565
566 def SubmitOpCode(op, cl=None, feedback_fn=None):
567   """Legacy function to submit an opcode.
568
569   This is just a simple wrapper over the construction of the processor
570   instance. It should be extended to better handle feedback and
571   interaction functions.
572
573   """
574   if cl is None:
575     cl = GetClient()
576
577   job_id = SendJob([op], cl)
578
579   op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
580
581   return op_results[0]
582
583
584 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
585   """Wrapper around SubmitOpCode or SendJob.
586
587   This function will decide, based on the 'opts' parameter, whether to
588   submit and wait for the result of the opcode (and return it), or
589   whether to just send the job and print its identifier. It is used in
590   order to simplify the implementation of the '--submit' option.
591
592   """
593   if opts and opts.submit_only:
594     job_id = SendJob([op], cl=cl)
595     raise JobSubmittedException(job_id)
596   else:
597     return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
598
599
600 def GetClient():
601   # TODO: Cache object?
602   try:
603     client = luxi.Client()
604   except luxi.NoMasterError:
605     master, myself = ssconf.GetMasterAndMyself()
606     if master != myself:
607       raise errors.OpPrereqError("This is not the master node, please connect"
608                                  " to node '%s' and rerun the command" %
609                                  master)
610     else:
611       raise
612   return client
613
614
615 def FormatError(err):
616   """Return a formatted error message for a given error.
617
618   This function takes an exception instance and returns a tuple
619   consisting of two values: first, the recommended exit code, and
620   second, a string describing the error message (not
621   newline-terminated).
622
623   """
624   retcode = 1
625   obuf = StringIO()
626   msg = str(err)
627   if isinstance(err, errors.ConfigurationError):
628     txt = "Corrupt configuration file: %s" % msg
629     logger.Error(txt)
630     obuf.write(txt + "\n")
631     obuf.write("Aborting.")
632     retcode = 2
633   elif isinstance(err, errors.HooksAbort):
634     obuf.write("Failure: hooks execution failed:\n")
635     for node, script, out in err.args[0]:
636       if out:
637         obuf.write("  node: %s, script: %s, output: %s\n" %
638                    (node, script, out))
639       else:
640         obuf.write("  node: %s, script: %s (no output)\n" %
641                    (node, script))
642   elif isinstance(err, errors.HooksFailure):
643     obuf.write("Failure: hooks general failure: %s" % msg)
644   elif isinstance(err, errors.ResolverError):
645     this_host = utils.HostInfo.SysName()
646     if err.args[0] == this_host:
647       msg = "Failure: can't resolve my own hostname ('%s')"
648     else:
649       msg = "Failure: can't resolve hostname '%s'"
650     obuf.write(msg % err.args[0])
651   elif isinstance(err, errors.OpPrereqError):
652     obuf.write("Failure: prerequisites not met for this"
653                " operation:\n%s" % msg)
654   elif isinstance(err, errors.OpExecError):
655     obuf.write("Failure: command execution error:\n%s" % msg)
656   elif isinstance(err, errors.TagError):
657     obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
658   elif isinstance(err, errors.GenericError):
659     obuf.write("Unhandled Ganeti error: %s" % msg)
660   elif isinstance(err, luxi.NoMasterError):
661     obuf.write("Cannot communicate with the master daemon.\nIs it running"
662                " and listening for connections?")
663   elif isinstance(err, luxi.TimeoutError):
664     obuf.write("Timeout while talking to the master daemon. Error:\n"
665                "%s" % msg)
666   elif isinstance(err, luxi.ProtocolError):
667     obuf.write("Unhandled protocol error while talking to the master daemon:\n"
668                "%s" % msg)
669   elif isinstance(err, JobSubmittedException):
670     obuf.write("JobID: %s\n" % err.args[0])
671     retcode = 0
672   else:
673     obuf.write("Unhandled exception: %s" % msg)
674   return retcode, obuf.getvalue().rstrip('\n')
675
676
677 def GenericMain(commands, override=None, aliases=None):
678   """Generic main function for all the gnt-* commands.
679
680   Arguments:
681     - commands: a dictionary with a special structure, see the design doc
682                 for command line handling.
683     - override: if not None, we expect a dictionary with keys that will
684                 override command line options; this can be used to pass
685                 options from the scripts to generic functions
686     - aliases: dictionary with command aliases {'alias': 'target, ...}
687
688   """
689   # save the program name and the entire command line for later logging
690   if sys.argv:
691     binary = os.path.basename(sys.argv[0]) or sys.argv[0]
692     if len(sys.argv) >= 2:
693       binary += " " + sys.argv[1]
694       old_cmdline = " ".join(sys.argv[2:])
695     else:
696       old_cmdline = ""
697   else:
698     binary = "<unknown program>"
699     old_cmdline = ""
700
701   if aliases is None:
702     aliases = {}
703
704   func, options, args = _ParseArgs(sys.argv, commands, aliases)
705   if func is None: # parse error
706     return 1
707
708   if override is not None:
709     for key, val in override.iteritems():
710       setattr(options, key, val)
711
712   logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
713                       stderr_logging=True, program=binary)
714
715   utils.debug = options.debug
716
717   if old_cmdline:
718     logger.Info("run with arguments '%s'" % old_cmdline)
719   else:
720     logger.Info("run with no arguments")
721
722   try:
723     result = func(options, args)
724   except (errors.GenericError, luxi.ProtocolError), err:
725     result, err_msg = FormatError(err)
726     logger.ToStderr(err_msg)
727
728   return result
729
730
731 def GenerateTable(headers, fields, separator, data,
732                   numfields=None, unitfields=None):
733   """Prints a table with headers and different fields.
734
735   Args:
736     headers: Dict of header titles or None if no headers should be shown
737     fields: List of fields to show
738     separator: String used to separate fields or None for spaces
739     data: Data to be printed
740     numfields: List of fields to be aligned to right
741     unitfields: List of fields to be formatted as units
742
743   """
744   if numfields is None:
745     numfields = []
746   if unitfields is None:
747     unitfields = []
748
749   format_fields = []
750   for field in fields:
751     if headers and field not in headers:
752       raise errors.ProgrammerError("Missing header description for field '%s'"
753                                    % field)
754     if separator is not None:
755       format_fields.append("%s")
756     elif field in numfields:
757       format_fields.append("%*s")
758     else:
759       format_fields.append("%-*s")
760
761   if separator is None:
762     mlens = [0 for name in fields]
763     format = ' '.join(format_fields)
764   else:
765     format = separator.replace("%", "%%").join(format_fields)
766
767   for row in data:
768     for idx, val in enumerate(row):
769       if fields[idx] in unitfields:
770         try:
771           val = int(val)
772         except ValueError:
773           pass
774         else:
775           val = row[idx] = utils.FormatUnit(val)
776       val = row[idx] = str(val)
777       if separator is None:
778         mlens[idx] = max(mlens[idx], len(val))
779
780   result = []
781   if headers:
782     args = []
783     for idx, name in enumerate(fields):
784       hdr = headers[name]
785       if separator is None:
786         mlens[idx] = max(mlens[idx], len(hdr))
787         args.append(mlens[idx])
788       args.append(hdr)
789     result.append(format % tuple(args))
790
791   for line in data:
792     args = []
793     for idx in xrange(len(fields)):
794       if separator is None:
795         args.append(mlens[idx])
796       args.append(line[idx])
797     result.append(format % tuple(args))
798
799   return result
800
801
802 def FormatTimestamp(ts):
803   """Formats a given timestamp.
804
805   @type ts: timestamp
806   @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
807
808   @rtype: string
809   @returns: a string with the formatted timestamp
810
811   """
812   if not isinstance (ts, (tuple, list)) or len(ts) != 2:
813     return '?'
814   sec, usec = ts
815   return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
816
817
818 def ParseTimespec(value):
819   """Parse a time specification.
820
821   The following suffixed will be recognized:
822
823     - s: seconds
824     - m: minutes
825     - h: hours
826     - d: day
827     - w: weeks
828
829   Without any suffix, the value will be taken to be in seconds.
830
831   """
832   value = str(value)
833   if not value:
834     raise errors.OpPrereqError("Empty time specification passed")
835   suffix_map = {
836     's': 1,
837     'm': 60,
838     'h': 3600,
839     'd': 86400,
840     'w': 604800,
841     }
842   if value[-1] not in suffix_map:
843     try:
844       value = int(value)
845     except ValueError:
846       raise errors.OpPrereqError("Invalid time specification '%s'" % value)
847   else:
848     multiplier = suffix_map[value[-1]]
849     value = value[:-1]
850     if not value: # no data left after stripping the suffix
851       raise errors.OpPrereqError("Invalid time specification (only"
852                                  " suffix passed)")
853     try:
854       value = int(value) * multiplier
855     except ValueError:
856       raise errors.OpPrereqError("Invalid time specification '%s'" % value)
857   return value