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