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