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