8cfbeabbad727f041e11d01715effd69d63b6803
[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 from cStringIO import StringIO
30
31 from ganeti import utils
32 from ganeti import logger
33 from ganeti import errors
34 from ganeti import mcpu
35 from ganeti import constants
36 from ganeti import opcodes
37
38 from optparse import (OptionParser, make_option, TitledHelpFormatter,
39                       Option, OptionValueError, SUPPRESS_HELP)
40
41 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain", "SubmitOpCode",
42            "cli_option", "GenerateTable", "AskUser",
43            "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
44            "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT",
45            "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
46            "FormatError",
47            ]
48
49
50 def _ExtractTagsObject(opts, args):
51   """Extract the tag type object.
52
53   Note that this function will modify its args parameter.
54
55   """
56   if not hasattr(opts, "tag_type"):
57     raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
58   kind = opts.tag_type
59   if kind == constants.TAG_CLUSTER:
60     retval = kind, kind
61   elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
62     if not args:
63       raise errors.OpPrereqError("no arguments passed to the command")
64     name = args.pop(0)
65     retval = kind, name
66   else:
67     raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
68   return retval
69
70
71 def _ExtendTags(opts, args):
72   """Extend the args if a source file has been given.
73
74   This function will extend the tags with the contents of the file
75   passed in the 'tags_source' attribute of the opts parameter. A file
76   named '-' will be replaced by stdin.
77
78   """
79   fname = opts.tags_source
80   if fname is None:
81     return
82   if fname == "-":
83     new_fh = sys.stdin
84   else:
85     new_fh = open(fname, "r")
86   new_data = []
87   try:
88     # we don't use the nice 'new_data = [line.strip() for line in fh]'
89     # because of python bug 1633941
90     while True:
91       line = new_fh.readline()
92       if not line:
93         break
94       new_data.append(line.strip())
95   finally:
96     new_fh.close()
97   args.extend(new_data)
98
99
100 def ListTags(opts, args):
101   """List the tags on a given object.
102
103   This is a generic implementation that knows how to deal with all
104   three cases of tag objects (cluster, node, instance). The opts
105   argument is expected to contain a tag_type field denoting what
106   object type we work on.
107
108   """
109   kind, name = _ExtractTagsObject(opts, args)
110   op = opcodes.OpGetTags(kind=kind, name=name)
111   result = SubmitOpCode(op)
112   result = list(result)
113   result.sort()
114   for tag in result:
115     print tag
116
117
118 def AddTags(opts, args):
119   """Add tags on a given object.
120
121   This is a generic implementation that knows how to deal with all
122   three cases of tag objects (cluster, node, instance). The opts
123   argument is expected to contain a tag_type field denoting what
124   object type we work on.
125
126   """
127   kind, name = _ExtractTagsObject(opts, args)
128   _ExtendTags(opts, args)
129   if not args:
130     raise errors.OpPrereqError("No tags to be added")
131   op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
132   SubmitOpCode(op)
133
134
135 def RemoveTags(opts, args):
136   """Remove tags from a given object.
137
138   This is a generic implementation that knows how to deal with all
139   three cases of tag objects (cluster, node, instance). The opts
140   argument is expected to contain a tag_type field denoting what
141   object type we work on.
142
143   """
144   kind, name = _ExtractTagsObject(opts, args)
145   _ExtendTags(opts, args)
146   if not args:
147     raise errors.OpPrereqError("No tags to be removed")
148   op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
149   SubmitOpCode(op)
150
151
152 DEBUG_OPT = make_option("-d", "--debug", default=False,
153                         action="store_true",
154                         help="Turn debugging on")
155
156 NOHDR_OPT = make_option("--no-headers", default=False,
157                         action="store_true", dest="no_headers",
158                         help="Don't display column headers")
159
160 SEP_OPT = make_option("--separator", default=None,
161                       action="store", dest="separator",
162                       help="Separator between output fields"
163                       " (defaults to one space)")
164
165 USEUNITS_OPT = make_option("--human-readable", default=False,
166                            action="store_true", dest="human_readable",
167                            help="Print sizes in human readable format")
168
169 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
170                          type="string", help="Select output fields",
171                          metavar="FIELDS")
172
173 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
174                         default=False, help="Force the operation")
175
176 _LOCK_OPT = make_option("--lock-retries", default=None,
177                         type="int", help=SUPPRESS_HELP)
178
179 TAG_SRC_OPT = make_option("--from", dest="tags_source",
180                           default=None, help="File with tag names")
181
182 def ARGS_FIXED(val):
183   """Macro-like function denoting a fixed number of arguments"""
184   return -val
185
186
187 def ARGS_ATLEAST(val):
188   """Macro-like function denoting a minimum number of arguments"""
189   return val
190
191
192 ARGS_NONE = None
193 ARGS_ONE = ARGS_FIXED(1)
194 ARGS_ANY = ARGS_ATLEAST(0)
195
196
197 def check_unit(option, opt, value):
198   """OptParsers custom converter for units.
199
200   """
201   try:
202     return utils.ParseUnit(value)
203   except errors.UnitParseError, err:
204     raise OptionValueError("option %s: %s" % (opt, err))
205
206
207 class CliOption(Option):
208   """Custom option class for optparse.
209
210   """
211   TYPES = Option.TYPES + ("unit",)
212   TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
213   TYPE_CHECKER["unit"] = check_unit
214
215
216 # optparse.py sets make_option, so we do it for our own option class, too
217 cli_option = CliOption
218
219
220 def _ParseArgs(argv, commands):
221   """Parses the command line and return the function which must be
222   executed together with its arguments
223
224   Arguments:
225     argv: the command line
226
227     commands: dictionary with special contents, see the design doc for
228     cmdline handling
229
230   """
231   if len(argv) == 0:
232     binary = "<command>"
233   else:
234     binary = argv[0].split("/")[-1]
235
236   if len(argv) > 1 and argv[1] == "--version":
237     print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
238     # Quit right away. That way we don't have to care about this special
239     # argument. optparse.py does it the same.
240     sys.exit(0)
241
242   if len(argv) < 2 or argv[1] not in commands.keys():
243     # let's do a nice thing
244     sortedcmds = commands.keys()
245     sortedcmds.sort()
246     print ("Usage: %(bin)s {command} [options...] [argument...]"
247            "\n%(bin)s <command> --help to see details, or"
248            " man %(bin)s\n" % {"bin": binary})
249     # compute the max line length for cmd + usage
250     mlen = max([len(" %s %s" % (cmd, commands[cmd][3])) for cmd in commands])
251     mlen = min(60, mlen) # should not get here...
252     # and format a nice command list
253     print "Commands:"
254     for cmd in sortedcmds:
255       cmdstr = " %s %s" % (cmd, commands[cmd][3])
256       help_text = commands[cmd][4]
257       help_lines = textwrap.wrap(help_text, 79-3-mlen)
258       print "%-*s - %s" % (mlen, cmdstr,
259                                           help_lines.pop(0))
260       for line in help_lines:
261         print "%-*s   %s" % (mlen, "", line)
262     print
263     return None, None, None
264   cmd = argv.pop(1)
265   func, nargs, parser_opts, usage, description = commands[cmd]
266   parser_opts.append(_LOCK_OPT)
267   parser = OptionParser(option_list=parser_opts,
268                         description=description,
269                         formatter=TitledHelpFormatter(),
270                         usage="%%prog %s %s" % (cmd, usage))
271   parser.disable_interspersed_args()
272   options, args = parser.parse_args()
273   if nargs is None:
274     if len(args) != 0:
275       print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
276       return None, None, None
277   elif nargs < 0 and len(args) != -nargs:
278     print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
279                          (cmd, -nargs))
280     return None, None, None
281   elif nargs >= 0 and len(args) < nargs:
282     print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
283                          (cmd, nargs))
284     return None, None, None
285
286   return func, options, args
287
288
289 def AskUser(text, choices=None):
290   """Ask the user a question.
291
292   Args:
293     text - the question to ask.
294
295     choices - list with elements tuples (input_char, return_value,
296     description); if not given, it will default to: [('y', True,
297     'Perform the operation'), ('n', False, 'Do no do the operation')];
298     note that the '?' char is reserved for help
299
300   Returns: one of the return values from the choices list; if input is
301   not possible (i.e. not running with a tty, we return the last entry
302   from the list
303
304   """
305   if choices is None:
306     choices = [('y', True, 'Perform the operation'),
307                ('n', False, 'Do not perform the operation')]
308   if not choices or not isinstance(choices, list):
309     raise errors.ProgrammerError("Invalid choiches argument to AskUser")
310   for entry in choices:
311     if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
312       raise errors.ProgrammerError("Invalid choiches element to AskUser")
313
314   answer = choices[-1][1]
315   new_text = []
316   for line in text.splitlines():
317     new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
318   text = "\n".join(new_text)
319   try:
320     f = file("/dev/tty", "a+")
321   except IOError:
322     return answer
323   try:
324     chars = [entry[0] for entry in choices]
325     chars[-1] = "[%s]" % chars[-1]
326     chars.append('?')
327     maps = dict([(entry[0], entry[1]) for entry in choices])
328     while True:
329       f.write(text)
330       f.write('\n')
331       f.write("/".join(chars))
332       f.write(": ")
333       line = f.readline(2).strip().lower()
334       if line in maps:
335         answer = maps[line]
336         break
337       elif line == '?':
338         for entry in choices:
339           f.write(" %s - %s\n" % (entry[0], entry[2]))
340         f.write("\n")
341         continue
342   finally:
343     f.close()
344   return answer
345
346
347 def SubmitOpCode(op, proc=None, feedback_fn=None):
348   """Function to submit an opcode.
349
350   This is just a simple wrapper over the construction of the processor
351   instance. It should be extended to better handle feedback and
352   interaction functions.
353
354   """
355   if feedback_fn is None:
356     feedback_fn = logger.ToStdout
357   if proc is None:
358     proc = mcpu.Processor(feedback=feedback_fn)
359   return proc.ExecOpCode(op)
360
361
362 def FormatError(err):
363   """Return a formatted error message for a given error.
364
365   This function takes an exception instance and returns a tuple
366   consisting of two values: first, the recommended exit code, and
367   second, a string describing the error message (not
368   newline-terminated).
369
370   """
371   retcode = 1
372   obuf = StringIO()
373   msg = str(err)
374   if isinstance(err, errors.ConfigurationError):
375     txt = "Corrupt configuration file: %s" % msg
376     logger.Error(txt)
377     obuf.write(txt + "\n")
378     obuf.write("Aborting.")
379     retcode = 2
380   elif isinstance(err, errors.HooksAbort):
381     obuf.write("Failure: hooks execution failed:\n")
382     for node, script, out in err.args[0]:
383       if out:
384         obuf.write("  node: %s, script: %s, output: %s\n" %
385                    (node, script, out))
386       else:
387         obuf.write("  node: %s, script: %s (no output)\n" %
388                    (node, script))
389   elif isinstance(err, errors.HooksFailure):
390     obuf.write("Failure: hooks general failure: %s" % msg)
391   elif isinstance(err, errors.ResolverError):
392     this_host = utils.HostInfo.SysName()
393     if err.args[0] == this_host:
394       msg = "Failure: can't resolve my own hostname ('%s')"
395     else:
396       msg = "Failure: can't resolve hostname '%s'"
397     obuf.write(msg % err.args[0])
398   elif isinstance(err, errors.OpPrereqError):
399     obuf.write("Failure: prerequisites not met for this"
400                " operation:\n%s" % msg)
401   elif isinstance(err, errors.OpExecError):
402     obuf.write("Failure: command execution error:\n%s" % msg)
403   elif isinstance(err, errors.TagError):
404     obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
405   elif isinstance(err, errors.GenericError):
406     obuf.write("Unhandled Ganeti error: %s" % msg)
407   else:
408     obuf.write("Unhandled exception: %s" % msg)
409   return retcode, obuf.getvalue().rstrip('\n')
410
411
412 def GenericMain(commands, override=None):
413   """Generic main function for all the gnt-* commands.
414
415   Arguments:
416     - commands: a dictionary with a special structure, see the design doc
417                 for command line handling.
418     - override: if not None, we expect a dictionary with keys that will
419                 override command line options; this can be used to pass
420                 options from the scripts to generic functions
421
422   """
423   # save the program name and the entire command line for later logging
424   if sys.argv:
425     binary = os.path.basename(sys.argv[0]) or sys.argv[0]
426     if len(sys.argv) >= 2:
427       binary += " " + sys.argv[1]
428       old_cmdline = " ".join(sys.argv[2:])
429     else:
430       old_cmdline = ""
431   else:
432     binary = "<unknown program>"
433     old_cmdline = ""
434
435   func, options, args = _ParseArgs(sys.argv, commands)
436   if func is None: # parse error
437     return 1
438
439   if override is not None:
440     for key, val in override.iteritems():
441       setattr(options, key, val)
442
443   logger.SetupLogging(debug=options.debug, program=binary)
444
445   try:
446     utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
447   except errors.LockError, err:
448     logger.ToStderr(str(err))
449     return 1
450
451   if old_cmdline:
452     logger.Info("run with arguments '%s'" % old_cmdline)
453   else:
454     logger.Info("run with no arguments")
455
456   try:
457     try:
458       result = func(options, args)
459     except errors.GenericError, err:
460       result, err_msg = FormatError(err)
461       logger.ToStderr(err_msg)
462   finally:
463     utils.Unlock('cmd')
464     utils.LockCleanup()
465
466   return result
467
468
469 def GenerateTable(headers, fields, separator, data,
470                   numfields=None, unitfields=None):
471   """Prints a table with headers and different fields.
472
473   Args:
474     headers: Dict of header titles or None if no headers should be shown
475     fields: List of fields to show
476     separator: String used to separate fields or None for spaces
477     data: Data to be printed
478     numfields: List of fields to be aligned to right
479     unitfields: List of fields to be formatted as units
480
481   """
482   if numfields is None:
483     numfields = []
484   if unitfields is None:
485     unitfields = []
486
487   format_fields = []
488   for field in fields:
489     if headers and field not in headers:
490       raise errors.ProgrammerError("Missing header description for field '%s'"
491                                    % field)
492     if separator is not None:
493       format_fields.append("%s")
494     elif field in numfields:
495       format_fields.append("%*s")
496     else:
497       format_fields.append("%-*s")
498
499   if separator is None:
500     mlens = [0 for name in fields]
501     format = ' '.join(format_fields)
502   else:
503     format = separator.replace("%", "%%").join(format_fields)
504
505   for row in data:
506     for idx, val in enumerate(row):
507       if fields[idx] in unitfields:
508         try:
509           val = int(val)
510         except ValueError:
511           pass
512         else:
513           val = row[idx] = utils.FormatUnit(val)
514       val = row[idx] = str(val)
515       if separator is None:
516         mlens[idx] = max(mlens[idx], len(val))
517
518   result = []
519   if headers:
520     args = []
521     for idx, name in enumerate(fields):
522       hdr = headers[name]
523       if separator is None:
524         mlens[idx] = max(mlens[idx], len(hdr))
525         args.append(mlens[idx])
526       args.append(hdr)
527     result.append(format % tuple(args))
528
529   for line in data:
530     args = []
531     for idx in xrange(len(fields)):
532       if separator is None:
533         args.append(mlens[idx])
534       args.append(line[idx])
535     result.append(format % tuple(args))
536
537   return result