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