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