Add missing descriptions to {Add,Remove}EtcHostsEntry.
[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", "SplitNodeOption"
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
183 def ARGS_FIXED(val):
184   """Macro-like function denoting a fixed number of arguments"""
185   return -val
186
187
188 def ARGS_ATLEAST(val):
189   """Macro-like function denoting a minimum number of arguments"""
190   return val
191
192
193 ARGS_NONE = None
194 ARGS_ONE = ARGS_FIXED(1)
195 ARGS_ANY = ARGS_ATLEAST(0)
196
197
198 def check_unit(option, opt, value):
199   """OptParsers custom converter for units.
200
201   """
202   try:
203     return utils.ParseUnit(value)
204   except errors.UnitParseError, err:
205     raise OptionValueError("option %s: %s" % (opt, err))
206
207
208 class CliOption(Option):
209   """Custom option class for optparse.
210
211   """
212   TYPES = Option.TYPES + ("unit",)
213   TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
214   TYPE_CHECKER["unit"] = check_unit
215
216
217 # optparse.py sets make_option, so we do it for our own option class, too
218 cli_option = CliOption
219
220
221 def _ParseArgs(argv, commands):
222   """Parses the command line and return the function which must be
223   executed together with its arguments
224
225   Arguments:
226     argv: the command line
227
228     commands: dictionary with special contents, see the design doc for
229     cmdline handling
230
231   """
232   if len(argv) == 0:
233     binary = "<command>"
234   else:
235     binary = argv[0].split("/")[-1]
236
237   if len(argv) > 1 and argv[1] == "--version":
238     print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
239     # Quit right away. That way we don't have to care about this special
240     # argument. optparse.py does it the same.
241     sys.exit(0)
242
243   if len(argv) < 2 or argv[1] not in commands.keys():
244     # let's do a nice thing
245     sortedcmds = commands.keys()
246     sortedcmds.sort()
247     print ("Usage: %(bin)s {command} [options...] [argument...]"
248            "\n%(bin)s <command> --help to see details, or"
249            " man %(bin)s\n" % {"bin": binary})
250     # compute the max line length for cmd + usage
251     mlen = max([len(" %s %s" % (cmd, commands[cmd][3])) for cmd in commands])
252     mlen = min(60, mlen) # should not get here...
253     # and format a nice command list
254     print "Commands:"
255     for cmd in sortedcmds:
256       cmdstr = " %s %s" % (cmd, commands[cmd][3])
257       help_text = commands[cmd][4]
258       help_lines = textwrap.wrap(help_text, 79-3-mlen)
259       print "%-*s - %s" % (mlen, cmdstr,
260                                           help_lines.pop(0))
261       for line in help_lines:
262         print "%-*s   %s" % (mlen, "", line)
263     print
264     return None, None, None
265   cmd = argv.pop(1)
266   func, nargs, parser_opts, usage, description = commands[cmd]
267   parser_opts.append(_LOCK_OPT)
268   parser = OptionParser(option_list=parser_opts,
269                         description=description,
270                         formatter=TitledHelpFormatter(),
271                         usage="%%prog %s %s" % (cmd, usage))
272   parser.disable_interspersed_args()
273   options, args = parser.parse_args()
274   if nargs is None:
275     if len(args) != 0:
276       print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
277       return None, None, None
278   elif nargs < 0 and len(args) != -nargs:
279     print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
280                          (cmd, -nargs))
281     return None, None, None
282   elif nargs >= 0 and len(args) < nargs:
283     print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
284                          (cmd, nargs))
285     return None, None, None
286
287   return func, options, args
288
289
290 def SplitNodeOption(value):
291   """Splits the value of a --node option.
292
293   """
294   if value and ':' in value:
295     return value.split(':', 1)
296   else:
297     return (value, None)
298
299
300 def AskUser(text, choices=None):
301   """Ask the user a question.
302
303   Args:
304     text - the question to ask.
305
306     choices - list with elements tuples (input_char, return_value,
307     description); if not given, it will default to: [('y', True,
308     'Perform the operation'), ('n', False, 'Do no do the operation')];
309     note that the '?' char is reserved for help
310
311   Returns: one of the return values from the choices list; if input is
312   not possible (i.e. not running with a tty, we return the last entry
313   from the list
314
315   """
316   if choices is None:
317     choices = [('y', True, 'Perform the operation'),
318                ('n', False, 'Do not perform the operation')]
319   if not choices or not isinstance(choices, list):
320     raise errors.ProgrammerError("Invalid choiches argument to AskUser")
321   for entry in choices:
322     if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
323       raise errors.ProgrammerError("Invalid choiches element to AskUser")
324
325   answer = choices[-1][1]
326   new_text = []
327   for line in text.splitlines():
328     new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
329   text = "\n".join(new_text)
330   try:
331     f = file("/dev/tty", "a+")
332   except IOError:
333     return answer
334   try:
335     chars = [entry[0] for entry in choices]
336     chars[-1] = "[%s]" % chars[-1]
337     chars.append('?')
338     maps = dict([(entry[0], entry[1]) for entry in choices])
339     while True:
340       f.write(text)
341       f.write('\n')
342       f.write("/".join(chars))
343       f.write(": ")
344       line = f.readline(2).strip().lower()
345       if line in maps:
346         answer = maps[line]
347         break
348       elif line == '?':
349         for entry in choices:
350           f.write(" %s - %s\n" % (entry[0], entry[2]))
351         f.write("\n")
352         continue
353   finally:
354     f.close()
355   return answer
356
357
358 def SubmitOpCode(op, proc=None, feedback_fn=None):
359   """Function to submit an opcode.
360
361   This is just a simple wrapper over the construction of the processor
362   instance. It should be extended to better handle feedback and
363   interaction functions.
364
365   """
366   if feedback_fn is None:
367     feedback_fn = logger.ToStdout
368   if proc is None:
369     proc = mcpu.Processor(feedback=feedback_fn)
370   return proc.ExecOpCode(op)
371
372
373 def FormatError(err):
374   """Return a formatted error message for a given error.
375
376   This function takes an exception instance and returns a tuple
377   consisting of two values: first, the recommended exit code, and
378   second, a string describing the error message (not
379   newline-terminated).
380
381   """
382   retcode = 1
383   obuf = StringIO()
384   msg = str(err)
385   if isinstance(err, errors.ConfigurationError):
386     txt = "Corrupt configuration file: %s" % msg
387     logger.Error(txt)
388     obuf.write(txt + "\n")
389     obuf.write("Aborting.")
390     retcode = 2
391   elif isinstance(err, errors.HooksAbort):
392     obuf.write("Failure: hooks execution failed:\n")
393     for node, script, out in err.args[0]:
394       if out:
395         obuf.write("  node: %s, script: %s, output: %s\n" %
396                    (node, script, out))
397       else:
398         obuf.write("  node: %s, script: %s (no output)\n" %
399                    (node, script))
400   elif isinstance(err, errors.HooksFailure):
401     obuf.write("Failure: hooks general failure: %s" % msg)
402   elif isinstance(err, errors.ResolverError):
403     this_host = utils.HostInfo.SysName()
404     if err.args[0] == this_host:
405       msg = "Failure: can't resolve my own hostname ('%s')"
406     else:
407       msg = "Failure: can't resolve hostname '%s'"
408     obuf.write(msg % err.args[0])
409   elif isinstance(err, errors.OpPrereqError):
410     obuf.write("Failure: prerequisites not met for this"
411                " operation:\n%s" % msg)
412   elif isinstance(err, errors.OpExecError):
413     obuf.write("Failure: command execution error:\n%s" % msg)
414   elif isinstance(err, errors.TagError):
415     obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
416   elif isinstance(err, errors.GenericError):
417     obuf.write("Unhandled Ganeti error: %s" % msg)
418   else:
419     obuf.write("Unhandled exception: %s" % msg)
420   return retcode, obuf.getvalue().rstrip('\n')
421
422
423 def GenericMain(commands, override=None):
424   """Generic main function for all the gnt-* commands.
425
426   Arguments:
427     - commands: a dictionary with a special structure, see the design doc
428                 for command line handling.
429     - override: if not None, we expect a dictionary with keys that will
430                 override command line options; this can be used to pass
431                 options from the scripts to generic functions
432
433   """
434   # save the program name and the entire command line for later logging
435   if sys.argv:
436     binary = os.path.basename(sys.argv[0]) or sys.argv[0]
437     if len(sys.argv) >= 2:
438       binary += " " + sys.argv[1]
439       old_cmdline = " ".join(sys.argv[2:])
440     else:
441       old_cmdline = ""
442   else:
443     binary = "<unknown program>"
444     old_cmdline = ""
445
446   func, options, args = _ParseArgs(sys.argv, commands)
447   if func is None: # parse error
448     return 1
449
450   if override is not None:
451     for key, val in override.iteritems():
452       setattr(options, key, val)
453
454   logger.SetupLogging(debug=options.debug, program=binary)
455
456   try:
457     utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
458   except errors.LockError, err:
459     logger.ToStderr(str(err))
460     return 1
461
462   if old_cmdline:
463     logger.Info("run with arguments '%s'" % old_cmdline)
464   else:
465     logger.Info("run with no arguments")
466
467   try:
468     try:
469       result = func(options, args)
470     except errors.GenericError, err:
471       result, err_msg = FormatError(err)
472       logger.ToStderr(err_msg)
473   finally:
474     utils.Unlock('cmd')
475     utils.LockCleanup()
476
477   return result
478
479
480 def GenerateTable(headers, fields, separator, data,
481                   numfields=None, unitfields=None):
482   """Prints a table with headers and different fields.
483
484   Args:
485     headers: Dict of header titles or None if no headers should be shown
486     fields: List of fields to show
487     separator: String used to separate fields or None for spaces
488     data: Data to be printed
489     numfields: List of fields to be aligned to right
490     unitfields: List of fields to be formatted as units
491
492   """
493   if numfields is None:
494     numfields = []
495   if unitfields is None:
496     unitfields = []
497
498   format_fields = []
499   for field in fields:
500     if headers and field not in headers:
501       raise errors.ProgrammerError("Missing header description for field '%s'"
502                                    % field)
503     if separator is not None:
504       format_fields.append("%s")
505     elif field in numfields:
506       format_fields.append("%*s")
507     else:
508       format_fields.append("%-*s")
509
510   if separator is None:
511     mlens = [0 for name in fields]
512     format = ' '.join(format_fields)
513   else:
514     format = separator.replace("%", "%%").join(format_fields)
515
516   for row in data:
517     for idx, val in enumerate(row):
518       if fields[idx] in unitfields:
519         try:
520           val = int(val)
521         except ValueError:
522           pass
523         else:
524           val = row[idx] = utils.FormatUnit(val)
525       val = row[idx] = str(val)
526       if separator is None:
527         mlens[idx] = max(mlens[idx], len(val))
528
529   result = []
530   if headers:
531     args = []
532     for idx, name in enumerate(fields):
533       hdr = headers[name]
534       if separator is None:
535         mlens[idx] = max(mlens[idx], len(hdr))
536         args.append(mlens[idx])
537       args.append(hdr)
538     result.append(format % tuple(args))
539
540   for line in data:
541     args = []
542     for idx in xrange(len(fields)):
543       if separator is None:
544         args.append(mlens[idx])
545       args.append(line[idx])
546     result.append(format % tuple(args))
547
548   return result