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