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