cli: Add new classes for argument definitions
[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
45 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
46            "SubmitOpCode", "GetClient",
47            "cli_option",
48            "GenerateTable", "AskUser",
49            "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
50            "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT", "SUBMIT_OPT",
51            "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
52            "FormatError", "SplitNodeOption", "SubmitOrSend",
53            "JobSubmittedException", "FormatTimestamp", "ParseTimespec",
54            "ToStderr", "ToStdout", "UsesRPC",
55            "GetOnlineNodes", "JobExecutor", "SYNC_OPT", "CONFIRM_OPT",
56            "ArgJobId", "ArgSuggest", "ArgUnknown", "ArgFile", "ArgCommand",
57            "ArgInstance", "ArgNode", "ArgChoice",
58            ]
59
60 NO_PREFIX = "no_"
61 UN_PREFIX = "-"
62
63
64 class _Argument:
65   def __init__(self, min=0, max=None, suggest=None):
66     self.min = min
67     self.max = max
68
69   def __repr__(self):
70     return ("<%s min=%s max=%s>" %
71             (self.__class__.__name__, self.min, self.max))
72
73
74 class ArgSuggest(_Argument):
75   """Suggesting argument.
76
77   Value can be any of the ones passed to the constructor.
78
79   """
80   def __init__(self, min=0, max=None, choices=None):
81     _Argument.__init__(self, min=min, max=max)
82     self.choices = choices
83
84   def __repr__(self):
85     return ("<%s min=%s max=%s choices=%r>" %
86             (self.__class__.__name__, self.min, self.max, self.choices))
87
88
89 class ArgChoice(ArgSuggest):
90   """Choice argument.
91
92   Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
93   but value must be one of the choices.
94
95   """
96
97
98 class ArgUnknown(_Argument):
99   """Unknown argument to program (e.g. determined at runtime).
100
101   """
102
103
104 class ArgInstance(_Argument):
105   """Instances argument.
106
107   """
108
109
110 class ArgNode(_Argument):
111   """Node argument.
112
113   """
114
115 class ArgJobId(_Argument):
116   """Job ID argument.
117
118   """
119
120
121 class ArgFile(_Argument):
122   """File path argument.
123
124   """
125
126
127 class ArgCommand(_Argument):
128   """Command argument.
129
130   """
131
132
133 def _ExtractTagsObject(opts, args):
134   """Extract the tag type object.
135
136   Note that this function will modify its args parameter.
137
138   """
139   if not hasattr(opts, "tag_type"):
140     raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
141   kind = opts.tag_type
142   if kind == constants.TAG_CLUSTER:
143     retval = kind, kind
144   elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
145     if not args:
146       raise errors.OpPrereqError("no arguments passed to the command")
147     name = args.pop(0)
148     retval = kind, name
149   else:
150     raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
151   return retval
152
153
154 def _ExtendTags(opts, args):
155   """Extend the args if a source file has been given.
156
157   This function will extend the tags with the contents of the file
158   passed in the 'tags_source' attribute of the opts parameter. A file
159   named '-' will be replaced by stdin.
160
161   """
162   fname = opts.tags_source
163   if fname is None:
164     return
165   if fname == "-":
166     new_fh = sys.stdin
167   else:
168     new_fh = open(fname, "r")
169   new_data = []
170   try:
171     # we don't use the nice 'new_data = [line.strip() for line in fh]'
172     # because of python bug 1633941
173     while True:
174       line = new_fh.readline()
175       if not line:
176         break
177       new_data.append(line.strip())
178   finally:
179     new_fh.close()
180   args.extend(new_data)
181
182
183 def ListTags(opts, args):
184   """List the tags on a given object.
185
186   This is a generic implementation that knows how to deal with all
187   three cases of tag objects (cluster, node, instance). The opts
188   argument is expected to contain a tag_type field denoting what
189   object type we work on.
190
191   """
192   kind, name = _ExtractTagsObject(opts, args)
193   op = opcodes.OpGetTags(kind=kind, name=name)
194   result = SubmitOpCode(op)
195   result = list(result)
196   result.sort()
197   for tag in result:
198     ToStdout(tag)
199
200
201 def AddTags(opts, args):
202   """Add tags on a given object.
203
204   This is a generic implementation that knows how to deal with all
205   three cases of tag objects (cluster, node, instance). The opts
206   argument is expected to contain a tag_type field denoting what
207   object type we work on.
208
209   """
210   kind, name = _ExtractTagsObject(opts, args)
211   _ExtendTags(opts, args)
212   if not args:
213     raise errors.OpPrereqError("No tags to be added")
214   op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
215   SubmitOpCode(op)
216
217
218 def RemoveTags(opts, args):
219   """Remove tags from a given object.
220
221   This is a generic implementation that knows how to deal with all
222   three cases of tag objects (cluster, node, instance). The opts
223   argument is expected to contain a tag_type field denoting what
224   object type we work on.
225
226   """
227   kind, name = _ExtractTagsObject(opts, args)
228   _ExtendTags(opts, args)
229   if not args:
230     raise errors.OpPrereqError("No tags to be removed")
231   op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
232   SubmitOpCode(op)
233
234
235 DEBUG_OPT = make_option("-d", "--debug", default=False,
236                         action="store_true",
237                         help="Turn debugging on")
238
239 NOHDR_OPT = make_option("--no-headers", default=False,
240                         action="store_true", dest="no_headers",
241                         help="Don't display column headers")
242
243 SEP_OPT = make_option("--separator", default=None,
244                       action="store", dest="separator",
245                       help="Separator between output fields"
246                       " (defaults to one space)")
247
248 USEUNITS_OPT = make_option("--units", default=None,
249                            dest="units", choices=('h', 'm', 'g', 't'),
250                            help="Specify units for output (one of hmgt)")
251
252 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
253                          type="string", help="Comma separated list of"
254                          " output fields",
255                          metavar="FIELDS")
256
257 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
258                         default=False, help="Force the operation")
259
260 CONFIRM_OPT = make_option("--yes", dest="confirm", action="store_true",
261                           default=False, help="Do not require confirmation")
262
263 TAG_SRC_OPT = make_option("--from", dest="tags_source",
264                           default=None, help="File with tag names")
265
266 SUBMIT_OPT = make_option("--submit", dest="submit_only",
267                          default=False, action="store_true",
268                          help="Submit the job and return the job ID, but"
269                          " don't wait for the job to finish")
270
271 SYNC_OPT = make_option("--sync", dest="do_locking",
272                        default=False, action="store_true",
273                        help="Grab locks while doing the queries"
274                        " in order to ensure more consistent results")
275
276 _DRY_RUN_OPT = make_option("--dry-run", default=False,
277                           action="store_true",
278                           help="Do not execute the operation, just run the"
279                           " check steps and verify it it could be executed")
280
281
282 def ARGS_FIXED(val):
283   """Macro-like function denoting a fixed number of arguments"""
284   return -val
285
286
287 def ARGS_ATLEAST(val):
288   """Macro-like function denoting a minimum number of arguments"""
289   return val
290
291
292 ARGS_NONE = None
293 ARGS_ONE = ARGS_FIXED(1)
294 ARGS_ANY = ARGS_ATLEAST(0)
295
296
297 def check_unit(option, opt, value):
298   """OptParsers custom converter for units.
299
300   """
301   try:
302     return utils.ParseUnit(value)
303   except errors.UnitParseError, err:
304     raise OptionValueError("option %s: %s" % (opt, err))
305
306
307 def _SplitKeyVal(opt, data):
308   """Convert a KeyVal string into a dict.
309
310   This function will convert a key=val[,...] string into a dict. Empty
311   values will be converted specially: keys which have the prefix 'no_'
312   will have the value=False and the prefix stripped, the others will
313   have value=True.
314
315   @type opt: string
316   @param opt: a string holding the option name for which we process the
317       data, used in building error messages
318   @type data: string
319   @param data: a string of the format key=val,key=val,...
320   @rtype: dict
321   @return: {key=val, key=val}
322   @raises errors.ParameterError: if there are duplicate keys
323
324   """
325   kv_dict = {}
326   if data:
327     for elem in data.split(","):
328       if "=" in elem:
329         key, val = elem.split("=", 1)
330       else:
331         if elem.startswith(NO_PREFIX):
332           key, val = elem[len(NO_PREFIX):], False
333         elif elem.startswith(UN_PREFIX):
334           key, val = elem[len(UN_PREFIX):], None
335         else:
336           key, val = elem, True
337       if key in kv_dict:
338         raise errors.ParameterError("Duplicate key '%s' in option %s" %
339                                     (key, opt))
340       kv_dict[key] = val
341   return kv_dict
342
343
344 def check_ident_key_val(option, opt, value):
345   """Custom parser for ident:key=val,key=val options.
346
347   This will store the parsed values as a tuple (ident, {key: val}). As such,
348   multiple uses of this option via action=append is possible.
349
350   """
351   if ":" not in value:
352     ident, rest = value, ''
353   else:
354     ident, rest = value.split(":", 1)
355
356   if ident.startswith(NO_PREFIX):
357     if rest:
358       msg = "Cannot pass options when removing parameter groups: %s" % value
359       raise errors.ParameterError(msg)
360     retval = (ident[len(NO_PREFIX):], False)
361   elif ident.startswith(UN_PREFIX):
362     if rest:
363       msg = "Cannot pass options when removing parameter groups: %s" % value
364       raise errors.ParameterError(msg)
365     retval = (ident[len(UN_PREFIX):], None)
366   else:
367     kv_dict = _SplitKeyVal(opt, rest)
368     retval = (ident, kv_dict)
369   return retval
370
371
372 def check_key_val(option, opt, value):
373   """Custom parser class for key=val,key=val options.
374
375   This will store the parsed values as a dict {key: val}.
376
377   """
378   return _SplitKeyVal(opt, value)
379
380
381 class CliOption(Option):
382   """Custom option class for optparse.
383
384   """
385   ATTRS = Option.ATTRS + [
386     "completion_suggest",
387     ]
388   TYPES = Option.TYPES + (
389     "identkeyval",
390     "keyval",
391     "unit",
392     )
393   TYPE_CHECKER = Option.TYPE_CHECKER.copy()
394   TYPE_CHECKER["identkeyval"] = check_ident_key_val
395   TYPE_CHECKER["keyval"] = check_key_val
396   TYPE_CHECKER["unit"] = check_unit
397
398
399 # optparse.py sets make_option, so we do it for our own option class, too
400 cli_option = CliOption
401
402
403 def _ParseArgs(argv, commands, aliases):
404   """Parser for the command line arguments.
405
406   This function parses the arguments and returns the function which
407   must be executed together with its (modified) arguments.
408
409   @param argv: the command line
410   @param commands: dictionary with special contents, see the design
411       doc for cmdline handling
412   @param aliases: dictionary with command aliases {'alias': 'target, ...}
413
414   """
415   if len(argv) == 0:
416     binary = "<command>"
417   else:
418     binary = argv[0].split("/")[-1]
419
420   if len(argv) > 1 and argv[1] == "--version":
421     ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
422     # Quit right away. That way we don't have to care about this special
423     # argument. optparse.py does it the same.
424     sys.exit(0)
425
426   if len(argv) < 2 or not (argv[1] in commands or
427                            argv[1] in aliases):
428     # let's do a nice thing
429     sortedcmds = commands.keys()
430     sortedcmds.sort()
431
432     ToStdout("Usage: %s {command} [options...] [argument...]", binary)
433     ToStdout("%s <command> --help to see details, or man %s", binary, binary)
434     ToStdout("")
435
436     # compute the max line length for cmd + usage
437     mlen = max([len(" %s" % cmd) for cmd in commands])
438     mlen = min(60, mlen) # should not get here...
439
440     # and format a nice command list
441     ToStdout("Commands:")
442     for cmd in sortedcmds:
443       cmdstr = " %s" % (cmd,)
444       help_text = commands[cmd][4]
445       help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
446       ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
447       for line in help_lines:
448         ToStdout("%-*s   %s", mlen, "", line)
449
450     ToStdout("")
451
452     return None, None, None
453
454   # get command, unalias it, and look it up in commands
455   cmd = argv.pop(1)
456   if cmd in aliases:
457     if cmd in commands:
458       raise errors.ProgrammerError("Alias '%s' overrides an existing"
459                                    " command" % cmd)
460
461     if aliases[cmd] not in commands:
462       raise errors.ProgrammerError("Alias '%s' maps to non-existing"
463                                    " command '%s'" % (cmd, aliases[cmd]))
464
465     cmd = aliases[cmd]
466
467   func, nargs, parser_opts, usage, description = commands[cmd]
468   parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
469                         description=description,
470                         formatter=TitledHelpFormatter(),
471                         usage="%%prog %s %s" % (cmd, usage))
472   parser.disable_interspersed_args()
473   options, args = parser.parse_args()
474   if nargs is None:
475     if len(args) != 0:
476       ToStderr("Error: Command %s expects no arguments", cmd)
477       return None, None, None
478   elif nargs < 0 and len(args) != -nargs:
479     ToStderr("Error: Command %s expects %d argument(s)", cmd, -nargs)
480     return None, None, None
481   elif nargs >= 0 and len(args) < nargs:
482     ToStderr("Error: Command %s expects at least %d argument(s)", cmd, nargs)
483     return None, None, None
484
485   return func, options, args
486
487
488 def SplitNodeOption(value):
489   """Splits the value of a --node option.
490
491   """
492   if value and ':' in value:
493     return value.split(':', 1)
494   else:
495     return (value, None)
496
497
498 def UsesRPC(fn):
499   def wrapper(*args, **kwargs):
500     rpc.Init()
501     try:
502       return fn(*args, **kwargs)
503     finally:
504       rpc.Shutdown()
505   return wrapper
506
507
508 def AskUser(text, choices=None):
509   """Ask the user a question.
510
511   @param text: the question to ask
512
513   @param choices: list with elements tuples (input_char, return_value,
514       description); if not given, it will default to: [('y', True,
515       'Perform the operation'), ('n', False, 'Do no do the operation')];
516       note that the '?' char is reserved for help
517
518   @return: one of the return values from the choices list; if input is
519       not possible (i.e. not running with a tty, we return the last
520       entry from the list
521
522   """
523   if choices is None:
524     choices = [('y', True, 'Perform the operation'),
525                ('n', False, 'Do not perform the operation')]
526   if not choices or not isinstance(choices, list):
527     raise errors.ProgrammerError("Invalid choices argument to AskUser")
528   for entry in choices:
529     if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
530       raise errors.ProgrammerError("Invalid choices element to AskUser")
531
532   answer = choices[-1][1]
533   new_text = []
534   for line in text.splitlines():
535     new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
536   text = "\n".join(new_text)
537   try:
538     f = file("/dev/tty", "a+")
539   except IOError:
540     return answer
541   try:
542     chars = [entry[0] for entry in choices]
543     chars[-1] = "[%s]" % chars[-1]
544     chars.append('?')
545     maps = dict([(entry[0], entry[1]) for entry in choices])
546     while True:
547       f.write(text)
548       f.write('\n')
549       f.write("/".join(chars))
550       f.write(": ")
551       line = f.readline(2).strip().lower()
552       if line in maps:
553         answer = maps[line]
554         break
555       elif line == '?':
556         for entry in choices:
557           f.write(" %s - %s\n" % (entry[0], entry[2]))
558         f.write("\n")
559         continue
560   finally:
561     f.close()
562   return answer
563
564
565 class JobSubmittedException(Exception):
566   """Job was submitted, client should exit.
567
568   This exception has one argument, the ID of the job that was
569   submitted. The handler should print this ID.
570
571   This is not an error, just a structured way to exit from clients.
572
573   """
574
575
576 def SendJob(ops, cl=None):
577   """Function to submit an opcode without waiting for the results.
578
579   @type ops: list
580   @param ops: list of opcodes
581   @type cl: luxi.Client
582   @param cl: the luxi client to use for communicating with the master;
583              if None, a new client will be created
584
585   """
586   if cl is None:
587     cl = GetClient()
588
589   job_id = cl.SubmitJob(ops)
590
591   return job_id
592
593
594 def PollJob(job_id, cl=None, feedback_fn=None):
595   """Function to poll for the result of a job.
596
597   @type job_id: job identified
598   @param job_id: the job to poll for results
599   @type cl: luxi.Client
600   @param cl: the luxi client to use for communicating with the master;
601              if None, a new client will be created
602
603   """
604   if cl is None:
605     cl = GetClient()
606
607   prev_job_info = None
608   prev_logmsg_serial = None
609
610   while True:
611     result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
612                                  prev_logmsg_serial)
613     if not result:
614       # job not found, go away!
615       raise errors.JobLost("Job with id %s lost" % job_id)
616
617     # Split result, a tuple of (field values, log entries)
618     (job_info, log_entries) = result
619     (status, ) = job_info
620
621     if log_entries:
622       for log_entry in log_entries:
623         (serial, timestamp, _, message) = log_entry
624         if callable(feedback_fn):
625           feedback_fn(log_entry[1:])
626         else:
627           encoded = utils.SafeEncode(message)
628           ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
629         prev_logmsg_serial = max(prev_logmsg_serial, serial)
630
631     # TODO: Handle canceled and archived jobs
632     elif status in (constants.JOB_STATUS_SUCCESS,
633                     constants.JOB_STATUS_ERROR,
634                     constants.JOB_STATUS_CANCELING,
635                     constants.JOB_STATUS_CANCELED):
636       break
637
638     prev_job_info = job_info
639
640   jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
641   if not jobs:
642     raise errors.JobLost("Job with id %s lost" % job_id)
643
644   status, opstatus, result = jobs[0]
645   if status == constants.JOB_STATUS_SUCCESS:
646     return result
647   elif status in (constants.JOB_STATUS_CANCELING,
648                   constants.JOB_STATUS_CANCELED):
649     raise errors.OpExecError("Job was canceled")
650   else:
651     has_ok = False
652     for idx, (status, msg) in enumerate(zip(opstatus, result)):
653       if status == constants.OP_STATUS_SUCCESS:
654         has_ok = True
655       elif status == constants.OP_STATUS_ERROR:
656         if has_ok:
657           raise errors.OpExecError("partial failure (opcode %d): %s" %
658                                    (idx, msg))
659         else:
660           raise errors.OpExecError(str(msg))
661     # default failure mode
662     raise errors.OpExecError(result)
663
664
665 def SubmitOpCode(op, cl=None, feedback_fn=None):
666   """Legacy function to submit an opcode.
667
668   This is just a simple wrapper over the construction of the processor
669   instance. It should be extended to better handle feedback and
670   interaction functions.
671
672   """
673   if cl is None:
674     cl = GetClient()
675
676   job_id = SendJob([op], cl)
677
678   op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
679
680   return op_results[0]
681
682
683 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
684   """Wrapper around SubmitOpCode or SendJob.
685
686   This function will decide, based on the 'opts' parameter, whether to
687   submit and wait for the result of the opcode (and return it), or
688   whether to just send the job and print its identifier. It is used in
689   order to simplify the implementation of the '--submit' option.
690
691   It will also add the dry-run parameter from the options passed, if true.
692
693   """
694   if opts and opts.dry_run:
695     op.dry_run = opts.dry_run
696   if opts and opts.submit_only:
697     job_id = SendJob([op], cl=cl)
698     raise JobSubmittedException(job_id)
699   else:
700     return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
701
702
703 def GetClient():
704   # TODO: Cache object?
705   try:
706     client = luxi.Client()
707   except luxi.NoMasterError:
708     master, myself = ssconf.GetMasterAndMyself()
709     if master != myself:
710       raise errors.OpPrereqError("This is not the master node, please connect"
711                                  " to node '%s' and rerun the command" %
712                                  master)
713     else:
714       raise
715   return client
716
717
718 def FormatError(err):
719   """Return a formatted error message for a given error.
720
721   This function takes an exception instance and returns a tuple
722   consisting of two values: first, the recommended exit code, and
723   second, a string describing the error message (not
724   newline-terminated).
725
726   """
727   retcode = 1
728   obuf = StringIO()
729   msg = str(err)
730   if isinstance(err, errors.ConfigurationError):
731     txt = "Corrupt configuration file: %s" % msg
732     logging.error(txt)
733     obuf.write(txt + "\n")
734     obuf.write("Aborting.")
735     retcode = 2
736   elif isinstance(err, errors.HooksAbort):
737     obuf.write("Failure: hooks execution failed:\n")
738     for node, script, out in err.args[0]:
739       if out:
740         obuf.write("  node: %s, script: %s, output: %s\n" %
741                    (node, script, out))
742       else:
743         obuf.write("  node: %s, script: %s (no output)\n" %
744                    (node, script))
745   elif isinstance(err, errors.HooksFailure):
746     obuf.write("Failure: hooks general failure: %s" % msg)
747   elif isinstance(err, errors.ResolverError):
748     this_host = utils.HostInfo.SysName()
749     if err.args[0] == this_host:
750       msg = "Failure: can't resolve my own hostname ('%s')"
751     else:
752       msg = "Failure: can't resolve hostname '%s'"
753     obuf.write(msg % err.args[0])
754   elif isinstance(err, errors.OpPrereqError):
755     obuf.write("Failure: prerequisites not met for this"
756                " operation:\n%s" % msg)
757   elif isinstance(err, errors.OpExecError):
758     obuf.write("Failure: command execution error:\n%s" % msg)
759   elif isinstance(err, errors.TagError):
760     obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
761   elif isinstance(err, errors.JobQueueDrainError):
762     obuf.write("Failure: the job queue is marked for drain and doesn't"
763                " accept new requests\n")
764   elif isinstance(err, errors.JobQueueFull):
765     obuf.write("Failure: the job queue is full and doesn't accept new"
766                " job submissions until old jobs are archived\n")
767   elif isinstance(err, errors.TypeEnforcementError):
768     obuf.write("Parameter Error: %s" % msg)
769   elif isinstance(err, errors.ParameterError):
770     obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
771   elif isinstance(err, errors.GenericError):
772     obuf.write("Unhandled Ganeti error: %s" % msg)
773   elif isinstance(err, luxi.NoMasterError):
774     obuf.write("Cannot communicate with the master daemon.\nIs it running"
775                " and listening for connections?")
776   elif isinstance(err, luxi.TimeoutError):
777     obuf.write("Timeout while talking to the master daemon. Error:\n"
778                "%s" % msg)
779   elif isinstance(err, luxi.ProtocolError):
780     obuf.write("Unhandled protocol error while talking to the master daemon:\n"
781                "%s" % msg)
782   elif isinstance(err, JobSubmittedException):
783     obuf.write("JobID: %s\n" % err.args[0])
784     retcode = 0
785   else:
786     obuf.write("Unhandled exception: %s" % msg)
787   return retcode, obuf.getvalue().rstrip('\n')
788
789
790 def GenericMain(commands, override=None, aliases=None):
791   """Generic main function for all the gnt-* commands.
792
793   Arguments:
794     - commands: a dictionary with a special structure, see the design doc
795                 for command line handling.
796     - override: if not None, we expect a dictionary with keys that will
797                 override command line options; this can be used to pass
798                 options from the scripts to generic functions
799     - aliases: dictionary with command aliases {'alias': 'target, ...}
800
801   """
802   # save the program name and the entire command line for later logging
803   if sys.argv:
804     binary = os.path.basename(sys.argv[0]) or sys.argv[0]
805     if len(sys.argv) >= 2:
806       binary += " " + sys.argv[1]
807       old_cmdline = " ".join(sys.argv[2:])
808     else:
809       old_cmdline = ""
810   else:
811     binary = "<unknown program>"
812     old_cmdline = ""
813
814   if aliases is None:
815     aliases = {}
816
817   try:
818     func, options, args = _ParseArgs(sys.argv, commands, aliases)
819   except errors.ParameterError, err:
820     result, err_msg = FormatError(err)
821     ToStderr(err_msg)
822     return 1
823
824   if func is None: # parse error
825     return 1
826
827   if override is not None:
828     for key, val in override.iteritems():
829       setattr(options, key, val)
830
831   utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
832                      stderr_logging=True, program=binary)
833
834   if old_cmdline:
835     logging.info("run with arguments '%s'", old_cmdline)
836   else:
837     logging.info("run with no arguments")
838
839   try:
840     result = func(options, args)
841   except (errors.GenericError, luxi.ProtocolError,
842           JobSubmittedException), err:
843     result, err_msg = FormatError(err)
844     logging.exception("Error during command processing")
845     ToStderr(err_msg)
846
847   return result
848
849
850 def GenerateTable(headers, fields, separator, data,
851                   numfields=None, unitfields=None,
852                   units=None):
853   """Prints a table with headers and different fields.
854
855   @type headers: dict
856   @param headers: dictionary mapping field names to headers for
857       the table
858   @type fields: list
859   @param fields: the field names corresponding to each row in
860       the data field
861   @param separator: the separator to be used; if this is None,
862       the default 'smart' algorithm is used which computes optimal
863       field width, otherwise just the separator is used between
864       each field
865   @type data: list
866   @param data: a list of lists, each sublist being one row to be output
867   @type numfields: list
868   @param numfields: a list with the fields that hold numeric
869       values and thus should be right-aligned
870   @type unitfields: list
871   @param unitfields: a list with the fields that hold numeric
872       values that should be formatted with the units field
873   @type units: string or None
874   @param units: the units we should use for formatting, or None for
875       automatic choice (human-readable for non-separator usage, otherwise
876       megabytes); this is a one-letter string
877
878   """
879   if units is None:
880     if separator:
881       units = "m"
882     else:
883       units = "h"
884
885   if numfields is None:
886     numfields = []
887   if unitfields is None:
888     unitfields = []
889
890   numfields = utils.FieldSet(*numfields)
891   unitfields = utils.FieldSet(*unitfields)
892
893   format_fields = []
894   for field in fields:
895     if headers and field not in headers:
896       # TODO: handle better unknown fields (either revert to old
897       # style of raising exception, or deal more intelligently with
898       # variable fields)
899       headers[field] = field
900     if separator is not None:
901       format_fields.append("%s")
902     elif numfields.Matches(field):
903       format_fields.append("%*s")
904     else:
905       format_fields.append("%-*s")
906
907   if separator is None:
908     mlens = [0 for name in fields]
909     format = ' '.join(format_fields)
910   else:
911     format = separator.replace("%", "%%").join(format_fields)
912
913   for row in data:
914     if row is None:
915       continue
916     for idx, val in enumerate(row):
917       if unitfields.Matches(fields[idx]):
918         try:
919           val = int(val)
920         except ValueError:
921           pass
922         else:
923           val = row[idx] = utils.FormatUnit(val, units)
924       val = row[idx] = str(val)
925       if separator is None:
926         mlens[idx] = max(mlens[idx], len(val))
927
928   result = []
929   if headers:
930     args = []
931     for idx, name in enumerate(fields):
932       hdr = headers[name]
933       if separator is None:
934         mlens[idx] = max(mlens[idx], len(hdr))
935         args.append(mlens[idx])
936       args.append(hdr)
937     result.append(format % tuple(args))
938
939   for line in data:
940     args = []
941     if line is None:
942       line = ['-' for _ in fields]
943     for idx in xrange(len(fields)):
944       if separator is None:
945         args.append(mlens[idx])
946       args.append(line[idx])
947     result.append(format % tuple(args))
948
949   return result
950
951
952 def FormatTimestamp(ts):
953   """Formats a given timestamp.
954
955   @type ts: timestamp
956   @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
957
958   @rtype: string
959   @return: a string with the formatted timestamp
960
961   """
962   if not isinstance (ts, (tuple, list)) or len(ts) != 2:
963     return '?'
964   sec, usec = ts
965   return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
966
967
968 def ParseTimespec(value):
969   """Parse a time specification.
970
971   The following suffixed will be recognized:
972
973     - s: seconds
974     - m: minutes
975     - h: hours
976     - d: day
977     - w: weeks
978
979   Without any suffix, the value will be taken to be in seconds.
980
981   """
982   value = str(value)
983   if not value:
984     raise errors.OpPrereqError("Empty time specification passed")
985   suffix_map = {
986     's': 1,
987     'm': 60,
988     'h': 3600,
989     'd': 86400,
990     'w': 604800,
991     }
992   if value[-1] not in suffix_map:
993     try:
994       value = int(value)
995     except ValueError:
996       raise errors.OpPrereqError("Invalid time specification '%s'" % value)
997   else:
998     multiplier = suffix_map[value[-1]]
999     value = value[:-1]
1000     if not value: # no data left after stripping the suffix
1001       raise errors.OpPrereqError("Invalid time specification (only"
1002                                  " suffix passed)")
1003     try:
1004       value = int(value) * multiplier
1005     except ValueError:
1006       raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1007   return value
1008
1009
1010 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1011   """Returns the names of online nodes.
1012
1013   This function will also log a warning on stderr with the names of
1014   the online nodes.
1015
1016   @param nodes: if not empty, use only this subset of nodes (minus the
1017       offline ones)
1018   @param cl: if not None, luxi client to use
1019   @type nowarn: boolean
1020   @param nowarn: by default, this function will output a note with the
1021       offline nodes that are skipped; if this parameter is True the
1022       note is not displayed
1023
1024   """
1025   if cl is None:
1026     cl = GetClient()
1027
1028   result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1029                          use_locking=False)
1030   offline = [row[0] for row in result if row[1]]
1031   if offline and not nowarn:
1032     ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1033   return [row[0] for row in result if not row[1]]
1034
1035
1036 def _ToStream(stream, txt, *args):
1037   """Write a message to a stream, bypassing the logging system
1038
1039   @type stream: file object
1040   @param stream: the file to which we should write
1041   @type txt: str
1042   @param txt: the message
1043
1044   """
1045   if args:
1046     args = tuple(args)
1047     stream.write(txt % args)
1048   else:
1049     stream.write(txt)
1050   stream.write('\n')
1051   stream.flush()
1052
1053
1054 def ToStdout(txt, *args):
1055   """Write a message to stdout only, bypassing the logging system
1056
1057   This is just a wrapper over _ToStream.
1058
1059   @type txt: str
1060   @param txt: the message
1061
1062   """
1063   _ToStream(sys.stdout, txt, *args)
1064
1065
1066 def ToStderr(txt, *args):
1067   """Write a message to stderr only, bypassing the logging system
1068
1069   This is just a wrapper over _ToStream.
1070
1071   @type txt: str
1072   @param txt: the message
1073
1074   """
1075   _ToStream(sys.stderr, txt, *args)
1076
1077
1078 class JobExecutor(object):
1079   """Class which manages the submission and execution of multiple jobs.
1080
1081   Note that instances of this class should not be reused between
1082   GetResults() calls.
1083
1084   """
1085   def __init__(self, cl=None, verbose=True):
1086     self.queue = []
1087     if cl is None:
1088       cl = GetClient()
1089     self.cl = cl
1090     self.verbose = verbose
1091     self.jobs = []
1092
1093   def QueueJob(self, name, *ops):
1094     """Record a job for later submit.
1095
1096     @type name: string
1097     @param name: a description of the job, will be used in WaitJobSet
1098     """
1099     self.queue.append((name, ops))
1100
1101   def SubmitPending(self):
1102     """Submit all pending jobs.
1103
1104     """
1105     results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1106     for ((status, data), (name, _)) in zip(results, self.queue):
1107       self.jobs.append((status, data, name))
1108
1109   def GetResults(self):
1110     """Wait for and return the results of all jobs.
1111
1112     @rtype: list
1113     @return: list of tuples (success, job results), in the same order
1114         as the submitted jobs; if a job has failed, instead of the result
1115         there will be the error message
1116
1117     """
1118     if not self.jobs:
1119       self.SubmitPending()
1120     results = []
1121     if self.verbose:
1122       ok_jobs = [row[1] for row in self.jobs if row[0]]
1123       if ok_jobs:
1124         ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1125     for submit_status, jid, name in self.jobs:
1126       if not submit_status:
1127         ToStderr("Failed to submit job for %s: %s", name, jid)
1128         results.append((False, jid))
1129         continue
1130       if self.verbose:
1131         ToStdout("Waiting for job %s for %s...", jid, name)
1132       try:
1133         job_result = PollJob(jid, cl=self.cl)
1134         success = True
1135       except (errors.GenericError, luxi.ProtocolError), err:
1136         _, job_result = FormatError(err)
1137         success = False
1138         # the error message will always be shown, verbose or not
1139         ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1140
1141       results.append((success, job_result))
1142     return results
1143
1144   def WaitOrShow(self, wait):
1145     """Wait for job results or only print the job IDs.
1146
1147     @type wait: boolean
1148     @param wait: whether to wait or not
1149
1150     """
1151     if wait:
1152       return self.GetResults()
1153     else:
1154       if not self.jobs:
1155         self.SubmitPending()
1156       for status, result, name in self.jobs:
1157         if status:
1158           ToStdout("%s: %s", result, name)
1159         else:
1160           ToStderr("Failure for %s: %s", name, result)