4 # Copyright (C) 2006, 2007 Google Inc.
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.
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.
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
22 """Module dealing with command line parsing"""
30 from cStringIO import StringIO
32 from ganeti import utils
33 from ganeti import logger
34 from ganeti import errors
35 from ganeti import constants
36 from ganeti import opcodes
37 from ganeti import luxi
38 from ganeti import ssconf
40 from optparse import (OptionParser, make_option, TitledHelpFormatter,
41 Option, OptionValueError)
43 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
44 "SubmitOpCode", "GetClient",
45 "cli_option", "GenerateTable", "AskUser",
46 "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
47 "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT",
48 "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
49 "FormatError", "SplitNodeOption"
53 def _ExtractTagsObject(opts, args):
54 """Extract the tag type object.
56 Note that this function will modify its args parameter.
59 if not hasattr(opts, "tag_type"):
60 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
62 if kind == constants.TAG_CLUSTER:
64 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
66 raise errors.OpPrereqError("no arguments passed to the command")
70 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
74 def _ExtendTags(opts, args):
75 """Extend the args if a source file has been given.
77 This function will extend the tags with the contents of the file
78 passed in the 'tags_source' attribute of the opts parameter. A file
79 named '-' will be replaced by stdin.
82 fname = opts.tags_source
88 new_fh = open(fname, "r")
91 # we don't use the nice 'new_data = [line.strip() for line in fh]'
92 # because of python bug 1633941
94 line = new_fh.readline()
97 new_data.append(line.strip())
100 args.extend(new_data)
103 def ListTags(opts, args):
104 """List the tags on a given object.
106 This is a generic implementation that knows how to deal with all
107 three cases of tag objects (cluster, node, instance). The opts
108 argument is expected to contain a tag_type field denoting what
109 object type we work on.
112 kind, name = _ExtractTagsObject(opts, args)
113 op = opcodes.OpGetTags(kind=kind, name=name)
114 result = SubmitOpCode(op)
115 result = list(result)
121 def AddTags(opts, args):
122 """Add tags on a given object.
124 This is a generic implementation that knows how to deal with all
125 three cases of tag objects (cluster, node, instance). The opts
126 argument is expected to contain a tag_type field denoting what
127 object type we work on.
130 kind, name = _ExtractTagsObject(opts, args)
131 _ExtendTags(opts, args)
133 raise errors.OpPrereqError("No tags to be added")
134 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
138 def RemoveTags(opts, args):
139 """Remove tags from a given object.
141 This is a generic implementation that knows how to deal with all
142 three cases of tag objects (cluster, node, instance). The opts
143 argument is expected to contain a tag_type field denoting what
144 object type we work on.
147 kind, name = _ExtractTagsObject(opts, args)
148 _ExtendTags(opts, args)
150 raise errors.OpPrereqError("No tags to be removed")
151 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
155 DEBUG_OPT = make_option("-d", "--debug", default=False,
157 help="Turn debugging on")
159 NOHDR_OPT = make_option("--no-headers", default=False,
160 action="store_true", dest="no_headers",
161 help="Don't display column headers")
163 SEP_OPT = make_option("--separator", default=None,
164 action="store", dest="separator",
165 help="Separator between output fields"
166 " (defaults to one space)")
168 USEUNITS_OPT = make_option("--human-readable", default=False,
169 action="store_true", dest="human_readable",
170 help="Print sizes in human readable format")
172 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
173 type="string", help="Comma separated list of"
177 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
178 default=False, help="Force the operation")
180 TAG_SRC_OPT = make_option("--from", dest="tags_source",
181 default=None, help="File with tag names")
185 """Macro-like function denoting a fixed number of arguments"""
189 def ARGS_ATLEAST(val):
190 """Macro-like function denoting a minimum number of arguments"""
195 ARGS_ONE = ARGS_FIXED(1)
196 ARGS_ANY = ARGS_ATLEAST(0)
199 def check_unit(option, opt, value):
200 """OptParsers custom converter for units.
204 return utils.ParseUnit(value)
205 except errors.UnitParseError, err:
206 raise OptionValueError("option %s: %s" % (opt, err))
209 class CliOption(Option):
210 """Custom option class for optparse.
213 TYPES = Option.TYPES + ("unit",)
214 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
215 TYPE_CHECKER["unit"] = check_unit
218 # optparse.py sets make_option, so we do it for our own option class, too
219 cli_option = CliOption
222 def _ParseArgs(argv, commands, aliases):
223 """Parses the command line and return the function which must be
224 executed together with its arguments
227 argv: the command line
229 commands: dictionary with special contents, see the design doc for
231 aliases: dictionary with command aliases {'alias': 'target, ...}
237 binary = argv[0].split("/")[-1]
239 if len(argv) > 1 and argv[1] == "--version":
240 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
241 # Quit right away. That way we don't have to care about this special
242 # argument. optparse.py does it the same.
245 if len(argv) < 2 or not (argv[1] in commands or
247 # let's do a nice thing
248 sortedcmds = commands.keys()
250 print ("Usage: %(bin)s {command} [options...] [argument...]"
251 "\n%(bin)s <command> --help to see details, or"
252 " man %(bin)s\n" % {"bin": binary})
253 # compute the max line length for cmd + usage
254 mlen = max([len(" %s" % cmd) for cmd in commands])
255 mlen = min(60, mlen) # should not get here...
256 # and format a nice command list
258 for cmd in sortedcmds:
259 cmdstr = " %s" % (cmd,)
260 help_text = commands[cmd][4]
261 help_lines = textwrap.wrap(help_text, 79-3-mlen)
262 print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
263 for line in help_lines:
264 print "%-*s %s" % (mlen, "", line)
266 return None, None, None
268 # get command, unalias it, and look it up in commands
272 raise errors.ProgrammerError("Alias '%s' overrides an existing"
275 if aliases[cmd] not in commands:
276 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
277 " command '%s'" % (cmd, aliases[cmd]))
281 func, nargs, parser_opts, usage, description = commands[cmd]
282 parser = OptionParser(option_list=parser_opts,
283 description=description,
284 formatter=TitledHelpFormatter(),
285 usage="%%prog %s %s" % (cmd, usage))
286 parser.disable_interspersed_args()
287 options, args = parser.parse_args()
290 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
291 return None, None, None
292 elif nargs < 0 and len(args) != -nargs:
293 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
295 return None, None, None
296 elif nargs >= 0 and len(args) < nargs:
297 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
299 return None, None, None
301 return func, options, args
304 def SplitNodeOption(value):
305 """Splits the value of a --node option.
308 if value and ':' in value:
309 return value.split(':', 1)
314 def AskUser(text, choices=None):
315 """Ask the user a question.
318 text - the question to ask.
320 choices - list with elements tuples (input_char, return_value,
321 description); if not given, it will default to: [('y', True,
322 'Perform the operation'), ('n', False, 'Do no do the operation')];
323 note that the '?' char is reserved for help
325 Returns: one of the return values from the choices list; if input is
326 not possible (i.e. not running with a tty, we return the last entry
331 choices = [('y', True, 'Perform the operation'),
332 ('n', False, 'Do not perform the operation')]
333 if not choices or not isinstance(choices, list):
334 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
335 for entry in choices:
336 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
337 raise errors.ProgrammerError("Invalid choiches element to AskUser")
339 answer = choices[-1][1]
341 for line in text.splitlines():
342 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
343 text = "\n".join(new_text)
345 f = file("/dev/tty", "a+")
349 chars = [entry[0] for entry in choices]
350 chars[-1] = "[%s]" % chars[-1]
352 maps = dict([(entry[0], entry[1]) for entry in choices])
356 f.write("/".join(chars))
358 line = f.readline(2).strip().lower()
363 for entry in choices:
364 f.write(" %s - %s\n" % (entry[0], entry[2]))
372 def SubmitOpCode(op, cl=None, feedback_fn=None):
373 """Legacy function to submit an opcode.
375 This is just a simple wrapper over the construction of the processor
376 instance. It should be extended to better handle feedback and
377 interaction functions.
383 job_id = cl.SubmitJob([op])
387 jobs = cl.QueryJobs([job_id], ["status", "ticker"])
389 # job not found, go away!
390 raise errors.JobLost("Job with id %s lost" % job_id)
392 # TODO: Handle canceled and archived jobs
394 if status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
397 if msg is not None and msg != lastmsg:
398 if callable(feedback_fn):
401 print "%s %s" % (time.ctime(msg[0]), msg[2])
405 jobs = cl.QueryJobs([job_id], ["status", "opresult"])
407 raise errors.JobLost("Job with id %s lost" % job_id)
409 status, result = jobs[0]
410 if status == constants.JOB_STATUS_SUCCESS:
413 raise errors.OpExecError(result)
417 # TODO: Cache object?
419 client = luxi.Client()
420 except luxi.NoMasterError:
421 master, myself = ssconf.GetMasterAndMyself()
423 raise errors.OpPrereqError("This is not the master node, please connect"
424 " to node '%s' and rerun the command" %
431 def FormatError(err):
432 """Return a formatted error message for a given error.
434 This function takes an exception instance and returns a tuple
435 consisting of two values: first, the recommended exit code, and
436 second, a string describing the error message (not
443 if isinstance(err, errors.ConfigurationError):
444 txt = "Corrupt configuration file: %s" % msg
446 obuf.write(txt + "\n")
447 obuf.write("Aborting.")
449 elif isinstance(err, errors.HooksAbort):
450 obuf.write("Failure: hooks execution failed:\n")
451 for node, script, out in err.args[0]:
453 obuf.write(" node: %s, script: %s, output: %s\n" %
456 obuf.write(" node: %s, script: %s (no output)\n" %
458 elif isinstance(err, errors.HooksFailure):
459 obuf.write("Failure: hooks general failure: %s" % msg)
460 elif isinstance(err, errors.ResolverError):
461 this_host = utils.HostInfo.SysName()
462 if err.args[0] == this_host:
463 msg = "Failure: can't resolve my own hostname ('%s')"
465 msg = "Failure: can't resolve hostname '%s'"
466 obuf.write(msg % err.args[0])
467 elif isinstance(err, errors.OpPrereqError):
468 obuf.write("Failure: prerequisites not met for this"
469 " operation:\n%s" % msg)
470 elif isinstance(err, errors.OpExecError):
471 obuf.write("Failure: command execution error:\n%s" % msg)
472 elif isinstance(err, errors.TagError):
473 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
474 elif isinstance(err, errors.GenericError):
475 obuf.write("Unhandled Ganeti error: %s" % msg)
476 elif isinstance(err, luxi.NoMasterError):
477 obuf.write("Cannot communicate with the master daemon.\nIs it running"
478 " and listening on '%s'?" % err.args[0])
479 elif isinstance(err, luxi.TimeoutError):
480 obuf.write("Timeout while talking to the master daemon. Error:\n"
482 elif isinstance(err, luxi.ProtocolError):
483 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
486 obuf.write("Unhandled exception: %s" % msg)
487 return retcode, obuf.getvalue().rstrip('\n')
490 def GenericMain(commands, override=None, aliases=None):
491 """Generic main function for all the gnt-* commands.
494 - commands: a dictionary with a special structure, see the design doc
495 for command line handling.
496 - override: if not None, we expect a dictionary with keys that will
497 override command line options; this can be used to pass
498 options from the scripts to generic functions
499 - aliases: dictionary with command aliases {'alias': 'target, ...}
502 # save the program name and the entire command line for later logging
504 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
505 if len(sys.argv) >= 2:
506 binary += " " + sys.argv[1]
507 old_cmdline = " ".join(sys.argv[2:])
511 binary = "<unknown program>"
517 func, options, args = _ParseArgs(sys.argv, commands, aliases)
518 if func is None: # parse error
521 if override is not None:
522 for key, val in override.iteritems():
523 setattr(options, key, val)
525 logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
526 stderr_logging=True, program=binary)
528 utils.debug = options.debug
531 logger.Info("run with arguments '%s'" % old_cmdline)
533 logger.Info("run with no arguments")
536 result = func(options, args)
537 except (errors.GenericError, luxi.ProtocolError), err:
538 result, err_msg = FormatError(err)
539 logger.ToStderr(err_msg)
544 def GenerateTable(headers, fields, separator, data,
545 numfields=None, unitfields=None):
546 """Prints a table with headers and different fields.
549 headers: Dict of header titles or None if no headers should be shown
550 fields: List of fields to show
551 separator: String used to separate fields or None for spaces
552 data: Data to be printed
553 numfields: List of fields to be aligned to right
554 unitfields: List of fields to be formatted as units
557 if numfields is None:
559 if unitfields is None:
564 if headers and field not in headers:
565 raise errors.ProgrammerError("Missing header description for field '%s'"
567 if separator is not None:
568 format_fields.append("%s")
569 elif field in numfields:
570 format_fields.append("%*s")
572 format_fields.append("%-*s")
574 if separator is None:
575 mlens = [0 for name in fields]
576 format = ' '.join(format_fields)
578 format = separator.replace("%", "%%").join(format_fields)
581 for idx, val in enumerate(row):
582 if fields[idx] in unitfields:
588 val = row[idx] = utils.FormatUnit(val)
589 val = row[idx] = str(val)
590 if separator is None:
591 mlens[idx] = max(mlens[idx], len(val))
596 for idx, name in enumerate(fields):
598 if separator is None:
599 mlens[idx] = max(mlens[idx], len(hdr))
600 args.append(mlens[idx])
602 result.append(format % tuple(args))
606 for idx in xrange(len(fields)):
607 if separator is None:
608 args.append(mlens[idx])
609 args.append(line[idx])
610 result.append(format % tuple(args))