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", "SUBMIT_OPT",
48 "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
49 "FormatError", "SplitNodeOption", "SubmitOrSend",
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")
183 SUBMIT_OPT = make_option("--submit", dest="submit_only",
184 default=False, action="store_true",
185 help="Submit the job and return the job ID, but"
186 " don't wait for the job to finish")
190 """Macro-like function denoting a fixed number of arguments"""
194 def ARGS_ATLEAST(val):
195 """Macro-like function denoting a minimum number of arguments"""
200 ARGS_ONE = ARGS_FIXED(1)
201 ARGS_ANY = ARGS_ATLEAST(0)
204 def check_unit(option, opt, value):
205 """OptParsers custom converter for units.
209 return utils.ParseUnit(value)
210 except errors.UnitParseError, err:
211 raise OptionValueError("option %s: %s" % (opt, err))
214 class CliOption(Option):
215 """Custom option class for optparse.
218 TYPES = Option.TYPES + ("unit",)
219 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
220 TYPE_CHECKER["unit"] = check_unit
223 # optparse.py sets make_option, so we do it for our own option class, too
224 cli_option = CliOption
227 def _ParseArgs(argv, commands, aliases):
228 """Parses the command line and return the function which must be
229 executed together with its arguments
232 argv: the command line
234 commands: dictionary with special contents, see the design doc for
236 aliases: dictionary with command aliases {'alias': 'target, ...}
242 binary = argv[0].split("/")[-1]
244 if len(argv) > 1 and argv[1] == "--version":
245 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
246 # Quit right away. That way we don't have to care about this special
247 # argument. optparse.py does it the same.
250 if len(argv) < 2 or not (argv[1] in commands or
252 # let's do a nice thing
253 sortedcmds = commands.keys()
255 print ("Usage: %(bin)s {command} [options...] [argument...]"
256 "\n%(bin)s <command> --help to see details, or"
257 " man %(bin)s\n" % {"bin": binary})
258 # compute the max line length for cmd + usage
259 mlen = max([len(" %s" % cmd) for cmd in commands])
260 mlen = min(60, mlen) # should not get here...
261 # and format a nice command list
263 for cmd in sortedcmds:
264 cmdstr = " %s" % (cmd,)
265 help_text = commands[cmd][4]
266 help_lines = textwrap.wrap(help_text, 79-3-mlen)
267 print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
268 for line in help_lines:
269 print "%-*s %s" % (mlen, "", line)
271 return None, None, None
273 # get command, unalias it, and look it up in commands
277 raise errors.ProgrammerError("Alias '%s' overrides an existing"
280 if aliases[cmd] not in commands:
281 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
282 " command '%s'" % (cmd, aliases[cmd]))
286 func, nargs, parser_opts, usage, description = commands[cmd]
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()
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)" %
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)" %
304 return None, None, None
306 return func, options, args
309 def SplitNodeOption(value):
310 """Splits the value of a --node option.
313 if value and ':' in value:
314 return value.split(':', 1)
319 def AskUser(text, choices=None):
320 """Ask the user a question.
323 text - the question to ask.
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
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
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")
344 answer = choices[-1][1]
346 for line in text.splitlines():
347 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
348 text = "\n".join(new_text)
350 f = file("/dev/tty", "a+")
354 chars = [entry[0] for entry in choices]
355 chars[-1] = "[%s]" % chars[-1]
357 maps = dict([(entry[0], entry[1]) for entry in choices])
361 f.write("/".join(chars))
363 line = f.readline(2).strip().lower()
368 for entry in choices:
369 f.write(" %s - %s\n" % (entry[0], entry[2]))
377 def SendJob(ops, cl=None):
378 """Function to submit an opcode without waiting for the results.
381 @param ops: list of opcodes
382 @type cl: luxi.Client
383 @param cl: the luxi client to use for communicating with the master;
384 if None, a new client will be created
390 job_id = cl.SubmitJob(ops)
395 def PollJob(job_id, cl=None, feedback_fn=None):
396 """Function to poll for the result of a job.
398 @type job_id: job identified
399 @param job_id: the job to poll for results
400 @type cl: luxi.Client
401 @param cl: the luxi client to use for communicating with the master;
402 if None, a new client will be created
410 jobs = cl.QueryJobs([job_id], ["status", "ticker"])
412 # job not found, go away!
413 raise errors.JobLost("Job with id %s lost" % job_id)
415 # TODO: Handle canceled and archived jobs
417 if status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
420 if msg is not None and msg != lastmsg:
421 if callable(feedback_fn):
424 print "%s %s" % (time.ctime(msg[0]), msg[2])
428 jobs = cl.QueryJobs([job_id], ["status", "opresult"])
430 raise errors.JobLost("Job with id %s lost" % job_id)
432 status, result = jobs[0]
433 if status == constants.JOB_STATUS_SUCCESS:
436 raise errors.OpExecError(result)
439 def SubmitOpCode(op, cl=None, feedback_fn=None):
440 """Legacy function to submit an opcode.
442 This is just a simple wrapper over the construction of the processor
443 instance. It should be extended to better handle feedback and
444 interaction functions.
450 job_id = SendJob([op], cl)
452 return PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
455 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
456 """Wrapper around SubmitOpCode or SendJob.
458 This function will decide, based on the 'opts' parameter, whether to
459 submit and wait for the result of the opcode (and return it), or
460 whether to just send the job and print its identifier. It is used in
461 order to simplify the implementation of the '--submit' option.
464 if opts and opts.submit_only:
465 print SendJob([op], cl=cl)
468 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
472 # TODO: Cache object?
474 client = luxi.Client()
475 except luxi.NoMasterError:
476 master, myself = ssconf.GetMasterAndMyself()
478 raise errors.OpPrereqError("This is not the master node, please connect"
479 " to node '%s' and rerun the command" %
486 def FormatError(err):
487 """Return a formatted error message for a given error.
489 This function takes an exception instance and returns a tuple
490 consisting of two values: first, the recommended exit code, and
491 second, a string describing the error message (not
498 if isinstance(err, errors.ConfigurationError):
499 txt = "Corrupt configuration file: %s" % msg
501 obuf.write(txt + "\n")
502 obuf.write("Aborting.")
504 elif isinstance(err, errors.HooksAbort):
505 obuf.write("Failure: hooks execution failed:\n")
506 for node, script, out in err.args[0]:
508 obuf.write(" node: %s, script: %s, output: %s\n" %
511 obuf.write(" node: %s, script: %s (no output)\n" %
513 elif isinstance(err, errors.HooksFailure):
514 obuf.write("Failure: hooks general failure: %s" % msg)
515 elif isinstance(err, errors.ResolverError):
516 this_host = utils.HostInfo.SysName()
517 if err.args[0] == this_host:
518 msg = "Failure: can't resolve my own hostname ('%s')"
520 msg = "Failure: can't resolve hostname '%s'"
521 obuf.write(msg % err.args[0])
522 elif isinstance(err, errors.OpPrereqError):
523 obuf.write("Failure: prerequisites not met for this"
524 " operation:\n%s" % msg)
525 elif isinstance(err, errors.OpExecError):
526 obuf.write("Failure: command execution error:\n%s" % msg)
527 elif isinstance(err, errors.TagError):
528 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
529 elif isinstance(err, errors.GenericError):
530 obuf.write("Unhandled Ganeti error: %s" % msg)
531 elif isinstance(err, luxi.NoMasterError):
532 obuf.write("Cannot communicate with the master daemon.\nIs it running"
533 " and listening on '%s'?" % err.args[0])
534 elif isinstance(err, luxi.TimeoutError):
535 obuf.write("Timeout while talking to the master daemon. Error:\n"
537 elif isinstance(err, luxi.ProtocolError):
538 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
541 obuf.write("Unhandled exception: %s" % msg)
542 return retcode, obuf.getvalue().rstrip('\n')
545 def GenericMain(commands, override=None, aliases=None):
546 """Generic main function for all the gnt-* commands.
549 - commands: a dictionary with a special structure, see the design doc
550 for command line handling.
551 - override: if not None, we expect a dictionary with keys that will
552 override command line options; this can be used to pass
553 options from the scripts to generic functions
554 - aliases: dictionary with command aliases {'alias': 'target, ...}
557 # save the program name and the entire command line for later logging
559 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
560 if len(sys.argv) >= 2:
561 binary += " " + sys.argv[1]
562 old_cmdline = " ".join(sys.argv[2:])
566 binary = "<unknown program>"
572 func, options, args = _ParseArgs(sys.argv, commands, aliases)
573 if func is None: # parse error
576 if override is not None:
577 for key, val in override.iteritems():
578 setattr(options, key, val)
580 logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
581 stderr_logging=True, program=binary)
583 utils.debug = options.debug
586 logger.Info("run with arguments '%s'" % old_cmdline)
588 logger.Info("run with no arguments")
591 result = func(options, args)
592 except (errors.GenericError, luxi.ProtocolError), err:
593 result, err_msg = FormatError(err)
594 logger.ToStderr(err_msg)
599 def GenerateTable(headers, fields, separator, data,
600 numfields=None, unitfields=None):
601 """Prints a table with headers and different fields.
604 headers: Dict of header titles or None if no headers should be shown
605 fields: List of fields to show
606 separator: String used to separate fields or None for spaces
607 data: Data to be printed
608 numfields: List of fields to be aligned to right
609 unitfields: List of fields to be formatted as units
612 if numfields is None:
614 if unitfields is None:
619 if headers and field not in headers:
620 raise errors.ProgrammerError("Missing header description for field '%s'"
622 if separator is not None:
623 format_fields.append("%s")
624 elif field in numfields:
625 format_fields.append("%*s")
627 format_fields.append("%-*s")
629 if separator is None:
630 mlens = [0 for name in fields]
631 format = ' '.join(format_fields)
633 format = separator.replace("%", "%%").join(format_fields)
636 for idx, val in enumerate(row):
637 if fields[idx] in unitfields:
643 val = row[idx] = utils.FormatUnit(val)
644 val = row[idx] = str(val)
645 if separator is None:
646 mlens[idx] = max(mlens[idx], len(val))
651 for idx, name in enumerate(fields):
653 if separator is None:
654 mlens[idx] = max(mlens[idx], len(hdr))
655 args.append(mlens[idx])
657 result.append(format % tuple(args))
661 for idx in xrange(len(fields)):
662 if separator is None:
663 args.append(mlens[idx])
664 args.append(line[idx])
665 result.append(format % tuple(args))