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
409 prev_logmsg_serial = None
412 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
415 # job not found, go away!
416 raise errors.JobLost("Job with id %s lost" % job_id)
418 # Split result, a tuple of (field values, log entries)
419 (job_info, log_entries) = result
420 (status, ) = job_info
423 for log_entry in log_entries:
424 (serial, timestamp, _, message) = log_entry
425 if callable(feedback_fn):
426 feedback_fn(log_entry[1:])
428 print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), message)
429 prev_logmsg_serial = max(prev_logmsg_serial, serial)
431 # TODO: Handle canceled and archived jobs
432 elif status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
435 prev_job_info = job_info
437 jobs = cl.QueryJobs([job_id], ["status", "opresult"])
439 raise errors.JobLost("Job with id %s lost" % job_id)
441 status, result = jobs[0]
442 if status == constants.JOB_STATUS_SUCCESS:
445 raise errors.OpExecError(result)
448 def SubmitOpCode(op, cl=None, feedback_fn=None):
449 """Legacy function to submit an opcode.
451 This is just a simple wrapper over the construction of the processor
452 instance. It should be extended to better handle feedback and
453 interaction functions.
459 job_id = SendJob([op], cl)
461 return PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
464 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
465 """Wrapper around SubmitOpCode or SendJob.
467 This function will decide, based on the 'opts' parameter, whether to
468 submit and wait for the result of the opcode (and return it), or
469 whether to just send the job and print its identifier. It is used in
470 order to simplify the implementation of the '--submit' option.
473 if opts and opts.submit_only:
474 print SendJob([op], cl=cl)
477 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
481 # TODO: Cache object?
483 client = luxi.Client()
484 except luxi.NoMasterError:
485 master, myself = ssconf.GetMasterAndMyself()
487 raise errors.OpPrereqError("This is not the master node, please connect"
488 " to node '%s' and rerun the command" %
495 def FormatError(err):
496 """Return a formatted error message for a given error.
498 This function takes an exception instance and returns a tuple
499 consisting of two values: first, the recommended exit code, and
500 second, a string describing the error message (not
507 if isinstance(err, errors.ConfigurationError):
508 txt = "Corrupt configuration file: %s" % msg
510 obuf.write(txt + "\n")
511 obuf.write("Aborting.")
513 elif isinstance(err, errors.HooksAbort):
514 obuf.write("Failure: hooks execution failed:\n")
515 for node, script, out in err.args[0]:
517 obuf.write(" node: %s, script: %s, output: %s\n" %
520 obuf.write(" node: %s, script: %s (no output)\n" %
522 elif isinstance(err, errors.HooksFailure):
523 obuf.write("Failure: hooks general failure: %s" % msg)
524 elif isinstance(err, errors.ResolverError):
525 this_host = utils.HostInfo.SysName()
526 if err.args[0] == this_host:
527 msg = "Failure: can't resolve my own hostname ('%s')"
529 msg = "Failure: can't resolve hostname '%s'"
530 obuf.write(msg % err.args[0])
531 elif isinstance(err, errors.OpPrereqError):
532 obuf.write("Failure: prerequisites not met for this"
533 " operation:\n%s" % msg)
534 elif isinstance(err, errors.OpExecError):
535 obuf.write("Failure: command execution error:\n%s" % msg)
536 elif isinstance(err, errors.TagError):
537 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
538 elif isinstance(err, errors.GenericError):
539 obuf.write("Unhandled Ganeti error: %s" % msg)
540 elif isinstance(err, luxi.NoMasterError):
541 obuf.write("Cannot communicate with the master daemon.\nIs it running"
542 " and listening for connections?")
543 elif isinstance(err, luxi.TimeoutError):
544 obuf.write("Timeout while talking to the master daemon. Error:\n"
546 elif isinstance(err, luxi.ProtocolError):
547 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
550 obuf.write("Unhandled exception: %s" % msg)
551 return retcode, obuf.getvalue().rstrip('\n')
554 def GenericMain(commands, override=None, aliases=None):
555 """Generic main function for all the gnt-* commands.
558 - commands: a dictionary with a special structure, see the design doc
559 for command line handling.
560 - override: if not None, we expect a dictionary with keys that will
561 override command line options; this can be used to pass
562 options from the scripts to generic functions
563 - aliases: dictionary with command aliases {'alias': 'target, ...}
566 # save the program name and the entire command line for later logging
568 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
569 if len(sys.argv) >= 2:
570 binary += " " + sys.argv[1]
571 old_cmdline = " ".join(sys.argv[2:])
575 binary = "<unknown program>"
581 func, options, args = _ParseArgs(sys.argv, commands, aliases)
582 if func is None: # parse error
585 if override is not None:
586 for key, val in override.iteritems():
587 setattr(options, key, val)
589 logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
590 stderr_logging=True, program=binary)
592 utils.debug = options.debug
595 logger.Info("run with arguments '%s'" % old_cmdline)
597 logger.Info("run with no arguments")
600 result = func(options, args)
601 except (errors.GenericError, luxi.ProtocolError), err:
602 result, err_msg = FormatError(err)
603 logger.ToStderr(err_msg)
608 def GenerateTable(headers, fields, separator, data,
609 numfields=None, unitfields=None):
610 """Prints a table with headers and different fields.
613 headers: Dict of header titles or None if no headers should be shown
614 fields: List of fields to show
615 separator: String used to separate fields or None for spaces
616 data: Data to be printed
617 numfields: List of fields to be aligned to right
618 unitfields: List of fields to be formatted as units
621 if numfields is None:
623 if unitfields is None:
628 if headers and field not in headers:
629 raise errors.ProgrammerError("Missing header description for field '%s'"
631 if separator is not None:
632 format_fields.append("%s")
633 elif field in numfields:
634 format_fields.append("%*s")
636 format_fields.append("%-*s")
638 if separator is None:
639 mlens = [0 for name in fields]
640 format = ' '.join(format_fields)
642 format = separator.replace("%", "%%").join(format_fields)
645 for idx, val in enumerate(row):
646 if fields[idx] in unitfields:
652 val = row[idx] = utils.FormatUnit(val)
653 val = row[idx] = str(val)
654 if separator is None:
655 mlens[idx] = max(mlens[idx], len(val))
660 for idx, name in enumerate(fields):
662 if separator is None:
663 mlens[idx] = max(mlens[idx], len(hdr))
664 args.append(mlens[idx])
666 result.append(format % tuple(args))
670 for idx in xrange(len(fields)):
671 if separator is None:
672 args.append(mlens[idx])
673 args.append(line[idx])
674 result.append(format % tuple(args))