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