Release ganeti 2.0~alpha1
[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("--units", default=None,
175                            dest="units", choices=('h', 'm', 'g', 't'),
176                            help="Specify units for output (one of hmgt)")
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   """Parser for the command line arguments.
318
319   This function parses the arguements and returns the function which
320   must be executed together with its (modified) arguments.
321
322   @param argv: the command line
323   @param commands: dictionary with special contents, see the design
324       doc for cmdline handling
325   @param 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   @param text: the question to ask
443
444   @param choices: list with elements tuples (input_char, return_value,
445       description); if not given, it will default to: [('y', True,
446       'Perform the operation'), ('n', False, 'Do no do the operation')];
447       note that the '?' char is reserved for help
448
449   @return: one of the return values from the choices list; if input is
450       not possible (i.e. not running with a tty, we return the last
451       entry from the list
452
453   """
454   if choices is None:
455     choices = [('y', True, 'Perform the operation'),
456                ('n', False, 'Do not perform the operation')]
457   if not choices or not isinstance(choices, list):
458     raise errors.ProgrammerError("Invalid choiches argument to AskUser")
459   for entry in choices:
460     if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
461       raise errors.ProgrammerError("Invalid choiches element to AskUser")
462
463   answer = choices[-1][1]
464   new_text = []
465   for line in text.splitlines():
466     new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
467   text = "\n".join(new_text)
468   try:
469     f = file("/dev/tty", "a+")
470   except IOError:
471     return answer
472   try:
473     chars = [entry[0] for entry in choices]
474     chars[-1] = "[%s]" % chars[-1]
475     chars.append('?')
476     maps = dict([(entry[0], entry[1]) for entry in choices])
477     while True:
478       f.write(text)
479       f.write('\n')
480       f.write("/".join(chars))
481       f.write(": ")
482       line = f.readline(2).strip().lower()
483       if line in maps:
484         answer = maps[line]
485         break
486       elif line == '?':
487         for entry in choices:
488           f.write(" %s - %s\n" % (entry[0], entry[2]))
489         f.write("\n")
490         continue
491   finally:
492     f.close()
493   return answer
494
495
496 class JobSubmittedException(Exception):
497   """Job was submitted, client should exit.
498
499   This exception has one argument, the ID of the job that was
500   submitted. The handler should print this ID.
501
502   This is not an error, just a structured way to exit from clients.
503
504   """
505
506
507 def SendJob(ops, cl=None):
508   """Function to submit an opcode without waiting for the results.
509
510   @type ops: list
511   @param ops: list of opcodes
512   @type cl: luxi.Client
513   @param cl: the luxi client to use for communicating with the master;
514              if None, a new client will be created
515
516   """
517   if cl is None:
518     cl = GetClient()
519
520   job_id = cl.SubmitJob(ops)
521
522   return job_id
523
524
525 def PollJob(job_id, cl=None, feedback_fn=None):
526   """Function to poll for the result of a job.
527
528   @type job_id: job identified
529   @param job_id: the job to poll for results
530   @type cl: luxi.Client
531   @param cl: the luxi client to use for communicating with the master;
532              if None, a new client will be created
533
534   """
535   if cl is None:
536     cl = GetClient()
537
538   prev_job_info = None
539   prev_logmsg_serial = None
540
541   while True:
542     result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
543                                  prev_logmsg_serial)
544     if not result:
545       # job not found, go away!
546       raise errors.JobLost("Job with id %s lost" % job_id)
547
548     # Split result, a tuple of (field values, log entries)
549     (job_info, log_entries) = result
550     (status, ) = job_info
551
552     if log_entries:
553       for log_entry in log_entries:
554         (serial, timestamp, _, message) = log_entry
555         if callable(feedback_fn):
556           feedback_fn(log_entry[1:])
557         else:
558           print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), message)
559         prev_logmsg_serial = max(prev_logmsg_serial, serial)
560
561     # TODO: Handle canceled and archived jobs
562     elif status in (constants.JOB_STATUS_SUCCESS,
563                     constants.JOB_STATUS_ERROR,
564                     constants.JOB_STATUS_CANCELING,
565                     constants.JOB_STATUS_CANCELED):
566       break
567
568     prev_job_info = job_info
569
570   jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
571   if not jobs:
572     raise errors.JobLost("Job with id %s lost" % job_id)
573
574   status, opstatus, result = jobs[0]
575   if status == constants.JOB_STATUS_SUCCESS:
576     return result
577   elif status in (constants.JOB_STATUS_CANCELING,
578                   constants.JOB_STATUS_CANCELED):
579     raise errors.OpExecError("Job was canceled")
580   else:
581     has_ok = False
582     for idx, (status, msg) in enumerate(zip(opstatus, result)):
583       if status == constants.OP_STATUS_SUCCESS:
584         has_ok = True
585       elif status == constants.OP_STATUS_ERROR:
586         if has_ok:
587           raise errors.OpExecError("partial failure (opcode %d): %s" %
588                                    (idx, msg))
589         else:
590           raise errors.OpExecError(str(msg))
591     # default failure mode
592     raise errors.OpExecError(result)
593
594
595 def SubmitOpCode(op, cl=None, feedback_fn=None):
596   """Legacy function to submit an opcode.
597
598   This is just a simple wrapper over the construction of the processor
599   instance. It should be extended to better handle feedback and
600   interaction functions.
601
602   """
603   if cl is None:
604     cl = GetClient()
605
606   job_id = SendJob([op], cl)
607
608   op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
609
610   return op_results[0]
611
612
613 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
614   """Wrapper around SubmitOpCode or SendJob.
615
616   This function will decide, based on the 'opts' parameter, whether to
617   submit and wait for the result of the opcode (and return it), or
618   whether to just send the job and print its identifier. It is used in
619   order to simplify the implementation of the '--submit' option.
620
621   """
622   if opts and opts.submit_only:
623     job_id = SendJob([op], cl=cl)
624     raise JobSubmittedException(job_id)
625   else:
626     return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
627
628
629 def GetClient():
630   # TODO: Cache object?
631   try:
632     client = luxi.Client()
633   except luxi.NoMasterError:
634     master, myself = ssconf.GetMasterAndMyself()
635     if master != myself:
636       raise errors.OpPrereqError("This is not the master node, please connect"
637                                  " to node '%s' and rerun the command" %
638                                  master)
639     else:
640       raise
641   return client
642
643
644 def FormatError(err):
645   """Return a formatted error message for a given error.
646
647   This function takes an exception instance and returns a tuple
648   consisting of two values: first, the recommended exit code, and
649   second, a string describing the error message (not
650   newline-terminated).
651
652   """
653   retcode = 1
654   obuf = StringIO()
655   msg = str(err)
656   if isinstance(err, errors.ConfigurationError):
657     txt = "Corrupt configuration file: %s" % msg
658     logging.error(txt)
659     obuf.write(txt + "\n")
660     obuf.write("Aborting.")
661     retcode = 2
662   elif isinstance(err, errors.HooksAbort):
663     obuf.write("Failure: hooks execution failed:\n")
664     for node, script, out in err.args[0]:
665       if out:
666         obuf.write("  node: %s, script: %s, output: %s\n" %
667                    (node, script, out))
668       else:
669         obuf.write("  node: %s, script: %s (no output)\n" %
670                    (node, script))
671   elif isinstance(err, errors.HooksFailure):
672     obuf.write("Failure: hooks general failure: %s" % msg)
673   elif isinstance(err, errors.ResolverError):
674     this_host = utils.HostInfo.SysName()
675     if err.args[0] == this_host:
676       msg = "Failure: can't resolve my own hostname ('%s')"
677     else:
678       msg = "Failure: can't resolve hostname '%s'"
679     obuf.write(msg % err.args[0])
680   elif isinstance(err, errors.OpPrereqError):
681     obuf.write("Failure: prerequisites not met for this"
682                " operation:\n%s" % msg)
683   elif isinstance(err, errors.OpExecError):
684     obuf.write("Failure: command execution error:\n%s" % msg)
685   elif isinstance(err, errors.TagError):
686     obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
687   elif isinstance(err, errors.JobQueueDrainError):
688     obuf.write("Failure: the job queue is marked for drain and doesn't"
689                " accept new requests\n")
690   elif isinstance(err, errors.GenericError):
691     obuf.write("Unhandled Ganeti error: %s" % msg)
692   elif isinstance(err, luxi.NoMasterError):
693     obuf.write("Cannot communicate with the master daemon.\nIs it running"
694                " and listening for connections?")
695   elif isinstance(err, luxi.TimeoutError):
696     obuf.write("Timeout while talking to the master daemon. Error:\n"
697                "%s" % msg)
698   elif isinstance(err, luxi.ProtocolError):
699     obuf.write("Unhandled protocol error while talking to the master daemon:\n"
700                "%s" % msg)
701   elif isinstance(err, JobSubmittedException):
702     obuf.write("JobID: %s\n" % err.args[0])
703     retcode = 0
704   else:
705     obuf.write("Unhandled exception: %s" % msg)
706   return retcode, obuf.getvalue().rstrip('\n')
707
708
709 def GenericMain(commands, override=None, aliases=None):
710   """Generic main function for all the gnt-* commands.
711
712   Arguments:
713     - commands: a dictionary with a special structure, see the design doc
714                 for command line handling.
715     - override: if not None, we expect a dictionary with keys that will
716                 override command line options; this can be used to pass
717                 options from the scripts to generic functions
718     - aliases: dictionary with command aliases {'alias': 'target, ...}
719
720   """
721   # save the program name and the entire command line for later logging
722   if sys.argv:
723     binary = os.path.basename(sys.argv[0]) or sys.argv[0]
724     if len(sys.argv) >= 2:
725       binary += " " + sys.argv[1]
726       old_cmdline = " ".join(sys.argv[2:])
727     else:
728       old_cmdline = ""
729   else:
730     binary = "<unknown program>"
731     old_cmdline = ""
732
733   if aliases is None:
734     aliases = {}
735
736   func, options, args = _ParseArgs(sys.argv, commands, aliases)
737   if func is None: # parse error
738     return 1
739
740   if override is not None:
741     for key, val in override.iteritems():
742       setattr(options, key, val)
743
744   utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
745                      stderr_logging=True, program=binary)
746
747   utils.debug = options.debug
748
749   if old_cmdline:
750     logging.info("run with arguments '%s'", old_cmdline)
751   else:
752     logging.info("run with no arguments")
753
754   try:
755     result = func(options, args)
756   except (errors.GenericError, luxi.ProtocolError,
757           JobSubmittedException), err:
758     result, err_msg = FormatError(err)
759     logging.exception("Error durring command processing")
760     ToStderr(err_msg)
761
762   return result
763
764
765 def GenerateTable(headers, fields, separator, data,
766                   numfields=None, unitfields=None,
767                   units=None):
768   """Prints a table with headers and different fields.
769
770   @type headers: dict
771   @param headers: dictionary mapping field names to headers for
772       the table
773   @type fields: list
774   @param fields: the field names corresponding to each row in
775       the data field
776   @param separator: the separator to be used; if this is None,
777       the default 'smart' algorithm is used which computes optimal
778       field width, otherwise just the separator is used between
779       each field
780   @type data: list
781   @param data: a list of lists, each sublist being one row to be output
782   @type numfields: list
783   @param numfields: a list with the fields that hold numeric
784       values and thus should be right-aligned
785   @type unitfields: list
786   @param unitfields: a list with the fields that hold numeric
787       values that should be formatted with the units field
788   @type units: string or None
789   @param units: the units we should use for formatting, or None for
790       automatic choice (human-readable for non-separator usage, otherwise
791       megabytes); this is a one-letter string
792
793   """
794   if units is None:
795     if separator:
796       units = "m"
797     else:
798       units = "h"
799
800   if numfields is None:
801     numfields = []
802   if unitfields is None:
803     unitfields = []
804
805   numfields = utils.FieldSet(*numfields)
806   unitfields = utils.FieldSet(*unitfields)
807
808   format_fields = []
809   for field in fields:
810     if headers and field not in headers:
811       # FIXME: handle better unknown fields (either revert to old
812       # style of raising exception, or deal more intelligently with
813       # variable fields)
814       headers[field] = field
815     if separator is not None:
816       format_fields.append("%s")
817     elif numfields.Matches(field):
818       format_fields.append("%*s")
819     else:
820       format_fields.append("%-*s")
821
822   if separator is None:
823     mlens = [0 for name in fields]
824     format = ' '.join(format_fields)
825   else:
826     format = separator.replace("%", "%%").join(format_fields)
827
828   for row in data:
829     for idx, val in enumerate(row):
830       if unitfields.Matches(fields[idx]):
831         try:
832           val = int(val)
833         except ValueError:
834           pass
835         else:
836           val = row[idx] = utils.FormatUnit(val, units)
837       val = row[idx] = str(val)
838       if separator is None:
839         mlens[idx] = max(mlens[idx], len(val))
840
841   result = []
842   if headers:
843     args = []
844     for idx, name in enumerate(fields):
845       hdr = headers[name]
846       if separator is None:
847         mlens[idx] = max(mlens[idx], len(hdr))
848         args.append(mlens[idx])
849       args.append(hdr)
850     result.append(format % tuple(args))
851
852   for line in data:
853     args = []
854     for idx in xrange(len(fields)):
855       if separator is None:
856         args.append(mlens[idx])
857       args.append(line[idx])
858     result.append(format % tuple(args))
859
860   return result
861
862
863 def FormatTimestamp(ts):
864   """Formats a given timestamp.
865
866   @type ts: timestamp
867   @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
868
869   @rtype: string
870   @returns: a string with the formatted timestamp
871
872   """
873   if not isinstance (ts, (tuple, list)) or len(ts) != 2:
874     return '?'
875   sec, usec = ts
876   return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
877
878
879 def ParseTimespec(value):
880   """Parse a time specification.
881
882   The following suffixed will be recognized:
883
884     - s: seconds
885     - m: minutes
886     - h: hours
887     - d: day
888     - w: weeks
889
890   Without any suffix, the value will be taken to be in seconds.
891
892   """
893   value = str(value)
894   if not value:
895     raise errors.OpPrereqError("Empty time specification passed")
896   suffix_map = {
897     's': 1,
898     'm': 60,
899     'h': 3600,
900     'd': 86400,
901     'w': 604800,
902     }
903   if value[-1] not in suffix_map:
904     try:
905       value = int(value)
906     except ValueError:
907       raise errors.OpPrereqError("Invalid time specification '%s'" % value)
908   else:
909     multiplier = suffix_map[value[-1]]
910     value = value[:-1]
911     if not value: # no data left after stripping the suffix
912       raise errors.OpPrereqError("Invalid time specification (only"
913                                  " suffix passed)")
914     try:
915       value = int(value) * multiplier
916     except ValueError:
917       raise errors.OpPrereqError("Invalid time specification '%s'" % value)
918   return value
919
920
921 def _ToStream(stream, txt, *args):
922   """Write a message to a stream, bypassing the logging system
923
924   @type stream: file object
925   @param stream: the file to which we should write
926   @type txt: str
927   @param txt: the message
928
929   """
930   if args:
931     args = tuple(args)
932     stream.write(txt % args)
933   else:
934     stream.write(txt)
935   stream.write('\n')
936   stream.flush()
937
938
939 def ToStdout(txt, *args):
940   """Write a message to stdout only, bypassing the logging system
941
942   This is just a wrapper over _ToStream.
943
944   @type txt: str
945   @param txt: the message
946
947   """
948   _ToStream(sys.stdout, txt, *args)
949
950
951 def ToStderr(txt, *args):
952   """Write a message to stderr only, bypassing the logging system
953
954   This is just a wrapper over _ToStream.
955
956   @type txt: str
957   @param txt: the message
958
959   """
960   _ToStream(sys.stderr, txt, *args)