Fix parsing of drbdsetup show output
[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   utils.debug = options.debug
457   try:
458     utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
459   except errors.LockError, err:
460     logger.ToStderr(str(err))
461     return 1
462
463   if old_cmdline:
464     logger.Info("run with arguments '%s'" % old_cmdline)
465   else:
466     logger.Info("run with no arguments")
467
468   try:
469     try:
470       result = func(options, args)
471     except errors.GenericError, err:
472       result, err_msg = FormatError(err)
473       logger.ToStderr(err_msg)
474   finally:
475     utils.Unlock('cmd')
476     utils.LockCleanup()
477
478   return result
479
480
481 def GenerateTable(headers, fields, separator, data,
482                   numfields=None, unitfields=None):
483   """Prints a table with headers and different fields.
484
485   Args:
486     headers: Dict of header titles or None if no headers should be shown
487     fields: List of fields to show
488     separator: String used to separate fields or None for spaces
489     data: Data to be printed
490     numfields: List of fields to be aligned to right
491     unitfields: List of fields to be formatted as units
492
493   """
494   if numfields is None:
495     numfields = []
496   if unitfields is None:
497     unitfields = []
498
499   format_fields = []
500   for field in fields:
501     if headers and field not in headers:
502       raise errors.ProgrammerError("Missing header description for field '%s'"
503                                    % field)
504     if separator is not None:
505       format_fields.append("%s")
506     elif field in numfields:
507       format_fields.append("%*s")
508     else:
509       format_fields.append("%-*s")
510
511   if separator is None:
512     mlens = [0 for name in fields]
513     format = ' '.join(format_fields)
514   else:
515     format = separator.replace("%", "%%").join(format_fields)
516
517   for row in data:
518     for idx, val in enumerate(row):
519       if fields[idx] in unitfields:
520         try:
521           val = int(val)
522         except ValueError:
523           pass
524         else:
525           val = row[idx] = utils.FormatUnit(val)
526       val = row[idx] = str(val)
527       if separator is None:
528         mlens[idx] = max(mlens[idx], len(val))
529
530   result = []
531   if headers:
532     args = []
533     for idx, name in enumerate(fields):
534       hdr = headers[name]
535       if separator is None:
536         mlens[idx] = max(mlens[idx], len(hdr))
537         args.append(mlens[idx])
538       args.append(hdr)
539     result.append(format % tuple(args))
540
541   for line in data:
542     args = []
543     for idx in xrange(len(fields)):
544       if separator is None:
545         args.append(mlens[idx])
546       args.append(line[idx])
547     result.append(format % tuple(args))
548
549   return result