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