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