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