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