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