make gnt-modify work with new HVM parameters
[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 %s" % (cmd, commands[cmd][3])) 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 %s" % (cmd, commands[cmd][3])
263       help_text = commands[cmd][4]
264       help_lines = textwrap.wrap(help_text, 79-3-mlen)
265       print "%-*s - %s" % (mlen, cmdstr,
266                                           help_lines.pop(0))
267       for line in help_lines:
268         print "%-*s   %s" % (mlen, "", line)
269     print
270     return None, None, None
271
272   # get command, unalias it, and look it up in commands
273   cmd = argv.pop(1)
274   if cmd in aliases:
275     if cmd in commands:
276       raise errors.ProgrammerError("Alias '%s' overrides an existing"
277                                    " command" % cmd)
278
279     if aliases[cmd] not in commands:
280       raise errors.ProgrammerError("Alias '%s' maps to non-existing"
281                                    " command '%s'" % (cmd, aliases[cmd]))
282
283     cmd = aliases[cmd]
284
285   func, nargs, parser_opts, usage, description = commands[cmd]
286   parser_opts.append(_LOCK_OPT)
287   parser_opts.append(_LOCK_NOAUTOCLEAN)
288   parser = OptionParser(option_list=parser_opts,
289                         description=description,
290                         formatter=TitledHelpFormatter(),
291                         usage="%%prog %s %s" % (cmd, usage))
292   parser.disable_interspersed_args()
293   options, args = parser.parse_args()
294   if nargs is None:
295     if len(args) != 0:
296       print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
297       return None, None, None
298   elif nargs < 0 and len(args) != -nargs:
299     print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
300                          (cmd, -nargs))
301     return None, None, None
302   elif nargs >= 0 and len(args) < nargs:
303     print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
304                          (cmd, nargs))
305     return None, None, None
306
307   return func, options, args
308
309
310 def SplitNodeOption(value):
311   """Splits the value of a --node option.
312
313   """
314   if value and ':' in value:
315     return value.split(':', 1)
316   else:
317     return (value, None)
318
319
320 def AskUser(text, choices=None):
321   """Ask the user a question.
322
323   Args:
324     text - the question to ask.
325
326     choices - list with elements tuples (input_char, return_value,
327     description); if not given, it will default to: [('y', True,
328     'Perform the operation'), ('n', False, 'Do no do the operation')];
329     note that the '?' char is reserved for help
330
331   Returns: one of the return values from the choices list; if input is
332   not possible (i.e. not running with a tty, we return the last entry
333   from the list
334
335   """
336   if choices is None:
337     choices = [('y', True, 'Perform the operation'),
338                ('n', False, 'Do not perform the operation')]
339   if not choices or not isinstance(choices, list):
340     raise errors.ProgrammerError("Invalid choiches argument to AskUser")
341   for entry in choices:
342     if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
343       raise errors.ProgrammerError("Invalid choiches element to AskUser")
344
345   answer = choices[-1][1]
346   new_text = []
347   for line in text.splitlines():
348     new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
349   text = "\n".join(new_text)
350   try:
351     f = file("/dev/tty", "a+")
352   except IOError:
353     return answer
354   try:
355     chars = [entry[0] for entry in choices]
356     chars[-1] = "[%s]" % chars[-1]
357     chars.append('?')
358     maps = dict([(entry[0], entry[1]) for entry in choices])
359     while True:
360       f.write(text)
361       f.write('\n')
362       f.write("/".join(chars))
363       f.write(": ")
364       line = f.readline(2).strip().lower()
365       if line in maps:
366         answer = maps[line]
367         break
368       elif line == '?':
369         for entry in choices:
370           f.write(" %s - %s\n" % (entry[0], entry[2]))
371         f.write("\n")
372         continue
373   finally:
374     f.close()
375   return answer
376
377
378 def SubmitOpCode(op, proc=None, feedback_fn=None):
379   """Function to submit an opcode.
380
381   This is just a simple wrapper over the construction of the processor
382   instance. It should be extended to better handle feedback and
383   interaction functions.
384
385   """
386   if feedback_fn is None:
387     feedback_fn = logger.ToStdout
388   if proc is None:
389     proc = mcpu.Processor(feedback=feedback_fn)
390   return proc.ExecOpCode(op)
391
392
393 def FormatError(err):
394   """Return a formatted error message for a given error.
395
396   This function takes an exception instance and returns a tuple
397   consisting of two values: first, the recommended exit code, and
398   second, a string describing the error message (not
399   newline-terminated).
400
401   """
402   retcode = 1
403   obuf = StringIO()
404   msg = str(err)
405   if isinstance(err, errors.ConfigurationError):
406     txt = "Corrupt configuration file: %s" % msg
407     logger.Error(txt)
408     obuf.write(txt + "\n")
409     obuf.write("Aborting.")
410     retcode = 2
411   elif isinstance(err, errors.HooksAbort):
412     obuf.write("Failure: hooks execution failed:\n")
413     for node, script, out in err.args[0]:
414       if out:
415         obuf.write("  node: %s, script: %s, output: %s\n" %
416                    (node, script, out))
417       else:
418         obuf.write("  node: %s, script: %s (no output)\n" %
419                    (node, script))
420   elif isinstance(err, errors.HooksFailure):
421     obuf.write("Failure: hooks general failure: %s" % msg)
422   elif isinstance(err, errors.ResolverError):
423     this_host = utils.HostInfo.SysName()
424     if err.args[0] == this_host:
425       msg = "Failure: can't resolve my own hostname ('%s')"
426     else:
427       msg = "Failure: can't resolve hostname '%s'"
428     obuf.write(msg % err.args[0])
429   elif isinstance(err, errors.OpPrereqError):
430     obuf.write("Failure: prerequisites not met for this"
431                " operation:\n%s" % msg)
432   elif isinstance(err, errors.OpExecError):
433     obuf.write("Failure: command execution error:\n%s" % msg)
434   elif isinstance(err, errors.TagError):
435     obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
436   elif isinstance(err, errors.GenericError):
437     obuf.write("Unhandled Ganeti error: %s" % msg)
438   else:
439     obuf.write("Unhandled exception: %s" % msg)
440   return retcode, obuf.getvalue().rstrip('\n')
441
442
443 def GenericMain(commands, override=None, aliases=None):
444   """Generic main function for all the gnt-* commands.
445
446   Arguments:
447     - commands: a dictionary with a special structure, see the design doc
448                 for command line handling.
449     - override: if not None, we expect a dictionary with keys that will
450                 override command line options; this can be used to pass
451                 options from the scripts to generic functions
452     - aliases: dictionary with command aliases {'alias': 'target, ...}
453
454   """
455   # save the program name and the entire command line for later logging
456   if sys.argv:
457     binary = os.path.basename(sys.argv[0]) or sys.argv[0]
458     if len(sys.argv) >= 2:
459       binary += " " + sys.argv[1]
460       old_cmdline = " ".join(sys.argv[2:])
461     else:
462       old_cmdline = ""
463   else:
464     binary = "<unknown program>"
465     old_cmdline = ""
466
467   if aliases is None:
468     aliases = {}
469
470   func, options, args = _ParseArgs(sys.argv, commands, aliases)
471   if func is None: # parse error
472     return 1
473
474   if override is not None:
475     for key, val in override.iteritems():
476       setattr(options, key, val)
477
478   logger.SetupLogging(debug=options.debug, program=binary)
479
480   utils.debug = options.debug
481   try:
482     utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug,
483                autoclean=not options.lock_noautoclean)
484   except errors.LockError, err:
485     logger.ToStderr(str(err))
486     return 1
487   except KeyboardInterrupt:
488     logger.ToStderr("Aborting.")
489     return 1
490
491   if old_cmdline:
492     logger.Info("run with arguments '%s'" % old_cmdline)
493   else:
494     logger.Info("run with no arguments")
495
496   try:
497     try:
498       result = func(options, args)
499     except errors.GenericError, err:
500       result, err_msg = FormatError(err)
501       logger.ToStderr(err_msg)
502   finally:
503     utils.Unlock('cmd')
504     utils.LockCleanup()
505
506   return result
507
508
509 def GenerateTable(headers, fields, separator, data,
510                   numfields=None, unitfields=None):
511   """Prints a table with headers and different fields.
512
513   Args:
514     headers: Dict of header titles or None if no headers should be shown
515     fields: List of fields to show
516     separator: String used to separate fields or None for spaces
517     data: Data to be printed
518     numfields: List of fields to be aligned to right
519     unitfields: List of fields to be formatted as units
520
521   """
522   if numfields is None:
523     numfields = []
524   if unitfields is None:
525     unitfields = []
526
527   format_fields = []
528   for field in fields:
529     if headers and field not in headers:
530       raise errors.ProgrammerError("Missing header description for field '%s'"
531                                    % field)
532     if separator is not None:
533       format_fields.append("%s")
534     elif field in numfields:
535       format_fields.append("%*s")
536     else:
537       format_fields.append("%-*s")
538
539   if separator is None:
540     mlens = [0 for name in fields]
541     format = ' '.join(format_fields)
542   else:
543     format = separator.replace("%", "%%").join(format_fields)
544
545   for row in data:
546     for idx, val in enumerate(row):
547       if fields[idx] in unitfields:
548         try:
549           val = int(val)
550         except ValueError:
551           pass
552         else:
553           val = row[idx] = utils.FormatUnit(val)
554       val = row[idx] = str(val)
555       if separator is None:
556         mlens[idx] = max(mlens[idx], len(val))
557
558   result = []
559   if headers:
560     args = []
561     for idx, name in enumerate(fields):
562       hdr = headers[name]
563       if separator is None:
564         mlens[idx] = max(mlens[idx], len(hdr))
565         args.append(mlens[idx])
566       args.append(hdr)
567     result.append(format % tuple(args))
568
569   for line in data:
570     args = []
571     for idx in xrange(len(fields)):
572       if separator is None:
573         args.append(mlens[idx])
574       args.append(line[idx])
575     result.append(format % tuple(args))
576
577   return result