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