Add two new options types for CLI usage
[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   kv_dict = {}
245   for elem in data.split(","):
246     if "=" in elem:
247       key, val = elem.split("=", 1)
248     else:
249       if elem.startswith(NO_PREFIX):
250         key, val = elem[len(NO_PREFIX):], False
251       else:
252         key, val = elem, True
253     if key in kv_dict:
254       raise errors.ParameterError("Duplicate key '%s' in option %s" %
255                                   (key, opt))
256     kv_dict[key] = val
257   return kv_dict
258
259
260 def check_ident_key_val(option, opt, value):
261   """Custom parser for the IdentKeyVal option type.
262
263   """
264   if ":" not in value:
265     retval =  (value, {})
266   else:
267     ident, rest = value.split(":", 1)
268     kv_dict = _SplitKeyVal(opt, rest)
269     retval = (ident, kv_dict)
270   return retval
271
272
273 class IdentKeyValOption(Option):
274   """Custom option class for ident:key=val,key=val options.
275
276   This will store the parsed values as a tuple (ident, {key: val}). As
277   such, multiple uses of this option via action=append is possible.
278
279   """
280   TYPES = Option.TYPES + ("identkeyval",)
281   TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
282   TYPE_CHECKER["identkeyval"] = check_ident_key_val
283
284
285 def check_key_val(option, opt, value):
286   """Custom parser for the KeyVal option type.
287
288   """
289   return _SplitKeyVal(opt, value)
290
291
292 class KeyValOption(Option):
293   """Custom option class for key=val,key=val options.
294
295   This will store the parsed values as a dict {key: val}.
296
297   """
298   TYPES = Option.TYPES + ("keyval",)
299   TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
300   TYPE_CHECKER["keyval"] = check_key_val
301
302
303 # optparse.py sets make_option, so we do it for our own option class, too
304 cli_option = CliOption
305 ikv_option = IdentKeyValOption
306 keyval_option = KeyValOption
307
308
309 def _ParseArgs(argv, commands, aliases):
310   """Parses the command line and return the function which must be
311   executed together with its arguments
312
313   Arguments:
314     argv: the command line
315
316     commands: dictionary with special contents, see the design doc for
317     cmdline handling
318     aliases: dictionary with command aliases {'alias': 'target, ...}
319
320   """
321   if len(argv) == 0:
322     binary = "<command>"
323   else:
324     binary = argv[0].split("/")[-1]
325
326   if len(argv) > 1 and argv[1] == "--version":
327     print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
328     # Quit right away. That way we don't have to care about this special
329     # argument. optparse.py does it the same.
330     sys.exit(0)
331
332   if len(argv) < 2 or not (argv[1] in commands or
333                            argv[1] in aliases):
334     # let's do a nice thing
335     sortedcmds = commands.keys()
336     sortedcmds.sort()
337     print ("Usage: %(bin)s {command} [options...] [argument...]"
338            "\n%(bin)s <command> --help to see details, or"
339            " man %(bin)s\n" % {"bin": binary})
340     # compute the max line length for cmd + usage
341     mlen = max([len(" %s" % cmd) for cmd in commands])
342     mlen = min(60, mlen) # should not get here...
343     # and format a nice command list
344     print "Commands:"
345     for cmd in sortedcmds:
346       cmdstr = " %s" % (cmd,)
347       help_text = commands[cmd][4]
348       help_lines = textwrap.wrap(help_text, 79-3-mlen)
349       print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
350       for line in help_lines:
351         print "%-*s   %s" % (mlen, "", line)
352     print
353     return None, None, None
354
355   # get command, unalias it, and look it up in commands
356   cmd = argv.pop(1)
357   if cmd in aliases:
358     if cmd in commands:
359       raise errors.ProgrammerError("Alias '%s' overrides an existing"
360                                    " command" % cmd)
361
362     if aliases[cmd] not in commands:
363       raise errors.ProgrammerError("Alias '%s' maps to non-existing"
364                                    " command '%s'" % (cmd, aliases[cmd]))
365
366     cmd = aliases[cmd]
367
368   func, nargs, parser_opts, usage, description = commands[cmd]
369   parser = OptionParser(option_list=parser_opts,
370                         description=description,
371                         formatter=TitledHelpFormatter(),
372                         usage="%%prog %s %s" % (cmd, usage))
373   parser.disable_interspersed_args()
374   options, args = parser.parse_args()
375   if nargs is None:
376     if len(args) != 0:
377       print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
378       return None, None, None
379   elif nargs < 0 and len(args) != -nargs:
380     print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
381                          (cmd, -nargs))
382     return None, None, None
383   elif nargs >= 0 and len(args) < nargs:
384     print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
385                          (cmd, nargs))
386     return None, None, None
387
388   return func, options, args
389
390
391 def SplitNodeOption(value):
392   """Splits the value of a --node option.
393
394   """
395   if value and ':' in value:
396     return value.split(':', 1)
397   else:
398     return (value, None)
399
400
401 def AskUser(text, choices=None):
402   """Ask the user a question.
403
404   Args:
405     text - the question to ask.
406
407     choices - list with elements tuples (input_char, return_value,
408     description); if not given, it will default to: [('y', True,
409     'Perform the operation'), ('n', False, 'Do no do the operation')];
410     note that the '?' char is reserved for help
411
412   Returns: one of the return values from the choices list; if input is
413   not possible (i.e. not running with a tty, we return the last entry
414   from the list
415
416   """
417   if choices is None:
418     choices = [('y', True, 'Perform the operation'),
419                ('n', False, 'Do not perform the operation')]
420   if not choices or not isinstance(choices, list):
421     raise errors.ProgrammerError("Invalid choiches argument to AskUser")
422   for entry in choices:
423     if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
424       raise errors.ProgrammerError("Invalid choiches element to AskUser")
425
426   answer = choices[-1][1]
427   new_text = []
428   for line in text.splitlines():
429     new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
430   text = "\n".join(new_text)
431   try:
432     f = file("/dev/tty", "a+")
433   except IOError:
434     return answer
435   try:
436     chars = [entry[0] for entry in choices]
437     chars[-1] = "[%s]" % chars[-1]
438     chars.append('?')
439     maps = dict([(entry[0], entry[1]) for entry in choices])
440     while True:
441       f.write(text)
442       f.write('\n')
443       f.write("/".join(chars))
444       f.write(": ")
445       line = f.readline(2).strip().lower()
446       if line in maps:
447         answer = maps[line]
448         break
449       elif line == '?':
450         for entry in choices:
451           f.write(" %s - %s\n" % (entry[0], entry[2]))
452         f.write("\n")
453         continue
454   finally:
455     f.close()
456   return answer
457
458
459 class JobSubmittedException(Exception):
460   """Job was submitted, client should exit.
461
462   This exception has one argument, the ID of the job that was
463   submitted. The handler should print this ID.
464
465   This is not an error, just a structured way to exit from clients.
466
467   """
468
469
470 def SendJob(ops, cl=None):
471   """Function to submit an opcode without waiting for the results.
472
473   @type ops: list
474   @param ops: list of opcodes
475   @type cl: luxi.Client
476   @param cl: the luxi client to use for communicating with the master;
477              if None, a new client will be created
478
479   """
480   if cl is None:
481     cl = GetClient()
482
483   job_id = cl.SubmitJob(ops)
484
485   return job_id
486
487
488 def PollJob(job_id, cl=None, feedback_fn=None):
489   """Function to poll for the result of a job.
490
491   @type job_id: job identified
492   @param job_id: the job to poll for results
493   @type cl: luxi.Client
494   @param cl: the luxi client to use for communicating with the master;
495              if None, a new client will be created
496
497   """
498   if cl is None:
499     cl = GetClient()
500
501   prev_job_info = None
502   prev_logmsg_serial = None
503
504   while True:
505     result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
506                                  prev_logmsg_serial)
507     if not result:
508       # job not found, go away!
509       raise errors.JobLost("Job with id %s lost" % job_id)
510
511     # Split result, a tuple of (field values, log entries)
512     (job_info, log_entries) = result
513     (status, ) = job_info
514
515     if log_entries:
516       for log_entry in log_entries:
517         (serial, timestamp, _, message) = log_entry
518         if callable(feedback_fn):
519           feedback_fn(log_entry[1:])
520         else:
521           print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), message)
522         prev_logmsg_serial = max(prev_logmsg_serial, serial)
523
524     # TODO: Handle canceled and archived jobs
525     elif status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
526       break
527
528     prev_job_info = job_info
529
530   jobs = cl.QueryJobs([job_id], ["status", "opresult"])
531   if not jobs:
532     raise errors.JobLost("Job with id %s lost" % job_id)
533
534   status, result = jobs[0]
535   if status == constants.JOB_STATUS_SUCCESS:
536     return result
537   else:
538     raise errors.OpExecError(result)
539
540
541 def SubmitOpCode(op, cl=None, feedback_fn=None):
542   """Legacy function to submit an opcode.
543
544   This is just a simple wrapper over the construction of the processor
545   instance. It should be extended to better handle feedback and
546   interaction functions.
547
548   """
549   if cl is None:
550     cl = GetClient()
551
552   job_id = SendJob([op], cl)
553
554   op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
555
556   return op_results[0]
557
558
559 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
560   """Wrapper around SubmitOpCode or SendJob.
561
562   This function will decide, based on the 'opts' parameter, whether to
563   submit and wait for the result of the opcode (and return it), or
564   whether to just send the job and print its identifier. It is used in
565   order to simplify the implementation of the '--submit' option.
566
567   """
568   if opts and opts.submit_only:
569     job_id = SendJob([op], cl=cl)
570     raise JobSubmittedException(job_id)
571   else:
572     return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
573
574
575 def GetClient():
576   # TODO: Cache object?
577   try:
578     client = luxi.Client()
579   except luxi.NoMasterError:
580     master, myself = ssconf.GetMasterAndMyself()
581     if master != myself:
582       raise errors.OpPrereqError("This is not the master node, please connect"
583                                  " to node '%s' and rerun the command" %
584                                  master)
585     else:
586       raise
587   return client
588
589
590 def FormatError(err):
591   """Return a formatted error message for a given error.
592
593   This function takes an exception instance and returns a tuple
594   consisting of two values: first, the recommended exit code, and
595   second, a string describing the error message (not
596   newline-terminated).
597
598   """
599   retcode = 1
600   obuf = StringIO()
601   msg = str(err)
602   if isinstance(err, errors.ConfigurationError):
603     txt = "Corrupt configuration file: %s" % msg
604     logger.Error(txt)
605     obuf.write(txt + "\n")
606     obuf.write("Aborting.")
607     retcode = 2
608   elif isinstance(err, errors.HooksAbort):
609     obuf.write("Failure: hooks execution failed:\n")
610     for node, script, out in err.args[0]:
611       if out:
612         obuf.write("  node: %s, script: %s, output: %s\n" %
613                    (node, script, out))
614       else:
615         obuf.write("  node: %s, script: %s (no output)\n" %
616                    (node, script))
617   elif isinstance(err, errors.HooksFailure):
618     obuf.write("Failure: hooks general failure: %s" % msg)
619   elif isinstance(err, errors.ResolverError):
620     this_host = utils.HostInfo.SysName()
621     if err.args[0] == this_host:
622       msg = "Failure: can't resolve my own hostname ('%s')"
623     else:
624       msg = "Failure: can't resolve hostname '%s'"
625     obuf.write(msg % err.args[0])
626   elif isinstance(err, errors.OpPrereqError):
627     obuf.write("Failure: prerequisites not met for this"
628                " operation:\n%s" % msg)
629   elif isinstance(err, errors.OpExecError):
630     obuf.write("Failure: command execution error:\n%s" % msg)
631   elif isinstance(err, errors.TagError):
632     obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
633   elif isinstance(err, errors.GenericError):
634     obuf.write("Unhandled Ganeti error: %s" % msg)
635   elif isinstance(err, luxi.NoMasterError):
636     obuf.write("Cannot communicate with the master daemon.\nIs it running"
637                " and listening for connections?")
638   elif isinstance(err, luxi.TimeoutError):
639     obuf.write("Timeout while talking to the master daemon. Error:\n"
640                "%s" % msg)
641   elif isinstance(err, luxi.ProtocolError):
642     obuf.write("Unhandled protocol error while talking to the master daemon:\n"
643                "%s" % msg)
644   elif isinstance(err, JobSubmittedException):
645     obuf.write("JobID: %s\n" % err.args[0])
646     retcode = 0
647   else:
648     obuf.write("Unhandled exception: %s" % msg)
649   return retcode, obuf.getvalue().rstrip('\n')
650
651
652 def GenericMain(commands, override=None, aliases=None):
653   """Generic main function for all the gnt-* commands.
654
655   Arguments:
656     - commands: a dictionary with a special structure, see the design doc
657                 for command line handling.
658     - override: if not None, we expect a dictionary with keys that will
659                 override command line options; this can be used to pass
660                 options from the scripts to generic functions
661     - aliases: dictionary with command aliases {'alias': 'target, ...}
662
663   """
664   # save the program name and the entire command line for later logging
665   if sys.argv:
666     binary = os.path.basename(sys.argv[0]) or sys.argv[0]
667     if len(sys.argv) >= 2:
668       binary += " " + sys.argv[1]
669       old_cmdline = " ".join(sys.argv[2:])
670     else:
671       old_cmdline = ""
672   else:
673     binary = "<unknown program>"
674     old_cmdline = ""
675
676   if aliases is None:
677     aliases = {}
678
679   func, options, args = _ParseArgs(sys.argv, commands, aliases)
680   if func is None: # parse error
681     return 1
682
683   if override is not None:
684     for key, val in override.iteritems():
685       setattr(options, key, val)
686
687   logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
688                       stderr_logging=True, program=binary)
689
690   utils.debug = options.debug
691
692   if old_cmdline:
693     logger.Info("run with arguments '%s'" % old_cmdline)
694   else:
695     logger.Info("run with no arguments")
696
697   try:
698     result = func(options, args)
699   except (errors.GenericError, luxi.ProtocolError), err:
700     result, err_msg = FormatError(err)
701     logger.ToStderr(err_msg)
702
703   return result
704
705
706 def GenerateTable(headers, fields, separator, data,
707                   numfields=None, unitfields=None):
708   """Prints a table with headers and different fields.
709
710   Args:
711     headers: Dict of header titles or None if no headers should be shown
712     fields: List of fields to show
713     separator: String used to separate fields or None for spaces
714     data: Data to be printed
715     numfields: List of fields to be aligned to right
716     unitfields: List of fields to be formatted as units
717
718   """
719   if numfields is None:
720     numfields = []
721   if unitfields is None:
722     unitfields = []
723
724   format_fields = []
725   for field in fields:
726     if headers and field not in headers:
727       raise errors.ProgrammerError("Missing header description for field '%s'"
728                                    % field)
729     if separator is not None:
730       format_fields.append("%s")
731     elif field in numfields:
732       format_fields.append("%*s")
733     else:
734       format_fields.append("%-*s")
735
736   if separator is None:
737     mlens = [0 for name in fields]
738     format = ' '.join(format_fields)
739   else:
740     format = separator.replace("%", "%%").join(format_fields)
741
742   for row in data:
743     for idx, val in enumerate(row):
744       if fields[idx] in unitfields:
745         try:
746           val = int(val)
747         except ValueError:
748           pass
749         else:
750           val = row[idx] = utils.FormatUnit(val)
751       val = row[idx] = str(val)
752       if separator is None:
753         mlens[idx] = max(mlens[idx], len(val))
754
755   result = []
756   if headers:
757     args = []
758     for idx, name in enumerate(fields):
759       hdr = headers[name]
760       if separator is None:
761         mlens[idx] = max(mlens[idx], len(hdr))
762         args.append(mlens[idx])
763       args.append(hdr)
764     result.append(format % tuple(args))
765
766   for line in data:
767     args = []
768     for idx in xrange(len(fields)):
769       if separator is None:
770         args.append(mlens[idx])
771       args.append(line[idx])
772     result.append(format % tuple(args))
773
774   return result
775
776
777 def FormatTimestamp(ts):
778   """Formats a given timestamp.
779
780   @type ts: timestamp
781   @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
782
783   @rtype: string
784   @returns: a string with the formatted timestamp
785
786   """
787   if not isinstance (ts, (tuple, list)) or len(ts) != 2:
788     return '?'
789   sec, usec = ts
790   return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
791
792
793 def ParseTimespec(value):
794   """Parse a time specification.
795
796   The following suffixed will be recognized:
797
798     - s: seconds
799     - m: minutes
800     - h: hours
801     - d: day
802     - w: weeks
803
804   Without any suffix, the value will be taken to be in seconds.
805
806   """
807   value = str(value)
808   if not value:
809     raise errors.OpPrereqError("Empty time specification passed")
810   suffix_map = {
811     's': 1,
812     'm': 60,
813     'h': 3600,
814     'd': 86400,
815     'w': 604800,
816     }
817   if value[-1] not in suffix_map:
818     try:
819       value = int(value)
820     except ValueError:
821       raise errors.OpPrereqError("Invalid time specification '%s'" % value)
822   else:
823     multiplier = suffix_map[value[-1]]
824     value = value[:-1]
825     if not value: # no data left after stripping the suffix
826       raise errors.OpPrereqError("Invalid time specification (only"
827                                  " suffix passed)")
828     try:
829       value = int(value) * multiplier
830     except ValueError:
831       raise errors.OpPrereqError("Invalid time specification '%s'" % value)
832   return value