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