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