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