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",
50 "JobSubmittedException", "FormatTimestamp",
54 def _ExtractTagsObject(opts, args):
55 """Extract the tag type object.
57 Note that this function will modify its args parameter.
60 if not hasattr(opts, "tag_type"):
61 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
63 if kind == constants.TAG_CLUSTER:
65 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
67 raise errors.OpPrereqError("no arguments passed to the command")
71 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
75 def _ExtendTags(opts, args):
76 """Extend the args if a source file has been given.
78 This function will extend the tags with the contents of the file
79 passed in the 'tags_source' attribute of the opts parameter. A file
80 named '-' will be replaced by stdin.
83 fname = opts.tags_source
89 new_fh = open(fname, "r")
92 # we don't use the nice 'new_data = [line.strip() for line in fh]'
93 # because of python bug 1633941
95 line = new_fh.readline()
98 new_data.append(line.strip())
101 args.extend(new_data)
104 def ListTags(opts, args):
105 """List the tags on a given object.
107 This is a generic implementation that knows how to deal with all
108 three cases of tag objects (cluster, node, instance). The opts
109 argument is expected to contain a tag_type field denoting what
110 object type we work on.
113 kind, name = _ExtractTagsObject(opts, args)
114 op = opcodes.OpGetTags(kind=kind, name=name)
115 result = SubmitOpCode(op)
116 result = list(result)
122 def AddTags(opts, args):
123 """Add tags on a given object.
125 This is a generic implementation that knows how to deal with all
126 three cases of tag objects (cluster, node, instance). The opts
127 argument is expected to contain a tag_type field denoting what
128 object type we work on.
131 kind, name = _ExtractTagsObject(opts, args)
132 _ExtendTags(opts, args)
134 raise errors.OpPrereqError("No tags to be added")
135 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
139 def RemoveTags(opts, args):
140 """Remove tags from a given object.
142 This is a generic implementation that knows how to deal with all
143 three cases of tag objects (cluster, node, instance). The opts
144 argument is expected to contain a tag_type field denoting what
145 object type we work on.
148 kind, name = _ExtractTagsObject(opts, args)
149 _ExtendTags(opts, args)
151 raise errors.OpPrereqError("No tags to be removed")
152 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
156 DEBUG_OPT = make_option("-d", "--debug", default=False,
158 help="Turn debugging on")
160 NOHDR_OPT = make_option("--no-headers", default=False,
161 action="store_true", dest="no_headers",
162 help="Don't display column headers")
164 SEP_OPT = make_option("--separator", default=None,
165 action="store", dest="separator",
166 help="Separator between output fields"
167 " (defaults to one space)")
169 USEUNITS_OPT = make_option("--human-readable", default=False,
170 action="store_true", dest="human_readable",
171 help="Print sizes in human readable format")
173 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
174 type="string", help="Comma separated list of"
178 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
179 default=False, help="Force the operation")
181 TAG_SRC_OPT = make_option("--from", dest="tags_source",
182 default=None, help="File with tag names")
184 SUBMIT_OPT = make_option("--submit", dest="submit_only",
185 default=False, action="store_true",
186 help="Submit the job and return the job ID, but"
187 " don't wait for the job to finish")
191 """Macro-like function denoting a fixed number of arguments"""
195 def ARGS_ATLEAST(val):
196 """Macro-like function denoting a minimum number of arguments"""
201 ARGS_ONE = ARGS_FIXED(1)
202 ARGS_ANY = ARGS_ATLEAST(0)
205 def check_unit(option, opt, value):
206 """OptParsers custom converter for units.
210 return utils.ParseUnit(value)
211 except errors.UnitParseError, err:
212 raise OptionValueError("option %s: %s" % (opt, err))
215 class CliOption(Option):
216 """Custom option class for optparse.
219 TYPES = Option.TYPES + ("unit",)
220 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
221 TYPE_CHECKER["unit"] = check_unit
224 # optparse.py sets make_option, so we do it for our own option class, too
225 cli_option = CliOption
228 def _ParseArgs(argv, commands, aliases):
229 """Parses the command line and return the function which must be
230 executed together with its arguments
233 argv: the command line
235 commands: dictionary with special contents, see the design doc for
237 aliases: dictionary with command aliases {'alias': 'target, ...}
243 binary = argv[0].split("/")[-1]
245 if len(argv) > 1 and argv[1] == "--version":
246 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
247 # Quit right away. That way we don't have to care about this special
248 # argument. optparse.py does it the same.
251 if len(argv) < 2 or not (argv[1] in commands or
253 # let's do a nice thing
254 sortedcmds = commands.keys()
256 print ("Usage: %(bin)s {command} [options...] [argument...]"
257 "\n%(bin)s <command> --help to see details, or"
258 " man %(bin)s\n" % {"bin": binary})
259 # compute the max line length for cmd + usage
260 mlen = max([len(" %s" % cmd) for cmd in commands])
261 mlen = min(60, mlen) # should not get here...
262 # and format a nice command list
264 for cmd in sortedcmds:
265 cmdstr = " %s" % (cmd,)
266 help_text = commands[cmd][4]
267 help_lines = textwrap.wrap(help_text, 79-3-mlen)
268 print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
269 for line in help_lines:
270 print "%-*s %s" % (mlen, "", line)
272 return None, None, None
274 # get command, unalias it, and look it up in commands
278 raise errors.ProgrammerError("Alias '%s' overrides an existing"
281 if aliases[cmd] not in commands:
282 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
283 " command '%s'" % (cmd, aliases[cmd]))
287 func, nargs, parser_opts, usage, description = commands[cmd]
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()
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)" %
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)" %
305 return None, None, None
307 return func, options, args
310 def SplitNodeOption(value):
311 """Splits the value of a --node option.
314 if value and ':' in value:
315 return value.split(':', 1)
320 def AskUser(text, choices=None):
321 """Ask the user a question.
324 text - the question to ask.
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
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
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")
345 answer = choices[-1][1]
347 for line in text.splitlines():
348 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
349 text = "\n".join(new_text)
351 f = file("/dev/tty", "a+")
355 chars = [entry[0] for entry in choices]
356 chars[-1] = "[%s]" % chars[-1]
358 maps = dict([(entry[0], entry[1]) for entry in choices])
362 f.write("/".join(chars))
364 line = f.readline(2).strip().lower()
369 for entry in choices:
370 f.write(" %s - %s\n" % (entry[0], entry[2]))
378 class JobSubmittedException(Exception):
379 """Job was submitted, client should exit.
381 This exception has one argument, the ID of the job that was
382 submitted. The handler should print this ID.
384 This is not an error, just a structured way to exit from clients.
389 def SendJob(ops, cl=None):
390 """Function to submit an opcode without waiting for the results.
393 @param ops: list of opcodes
394 @type cl: luxi.Client
395 @param cl: the luxi client to use for communicating with the master;
396 if None, a new client will be created
402 job_id = cl.SubmitJob(ops)
407 def PollJob(job_id, cl=None, feedback_fn=None):
408 """Function to poll for the result of a job.
410 @type job_id: job identified
411 @param job_id: the job to poll for results
412 @type cl: luxi.Client
413 @param cl: the luxi client to use for communicating with the master;
414 if None, a new client will be created
421 prev_logmsg_serial = None
424 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
427 # job not found, go away!
428 raise errors.JobLost("Job with id %s lost" % job_id)
430 # Split result, a tuple of (field values, log entries)
431 (job_info, log_entries) = result
432 (status, ) = job_info
435 for log_entry in log_entries:
436 (serial, timestamp, _, message) = log_entry
437 if callable(feedback_fn):
438 feedback_fn(log_entry[1:])
440 print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), message)
441 prev_logmsg_serial = max(prev_logmsg_serial, serial)
443 # TODO: Handle canceled and archived jobs
444 elif status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
447 prev_job_info = job_info
449 jobs = cl.QueryJobs([job_id], ["status", "opresult"])
451 raise errors.JobLost("Job with id %s lost" % job_id)
453 status, result = jobs[0]
454 if status == constants.JOB_STATUS_SUCCESS:
457 raise errors.OpExecError(result)
460 def SubmitOpCode(op, cl=None, feedback_fn=None):
461 """Legacy function to submit an opcode.
463 This is just a simple wrapper over the construction of the processor
464 instance. It should be extended to better handle feedback and
465 interaction functions.
471 job_id = SendJob([op], cl)
473 return PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
476 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
477 """Wrapper around SubmitOpCode or SendJob.
479 This function will decide, based on the 'opts' parameter, whether to
480 submit and wait for the result of the opcode (and return it), or
481 whether to just send the job and print its identifier. It is used in
482 order to simplify the implementation of the '--submit' option.
485 if opts and opts.submit_only:
486 job_id = SendJob([op], cl=cl)
487 raise JobSubmittedException(job_id)
489 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
493 # TODO: Cache object?
495 client = luxi.Client()
496 except luxi.NoMasterError:
497 master, myself = ssconf.GetMasterAndMyself()
499 raise errors.OpPrereqError("This is not the master node, please connect"
500 " to node '%s' and rerun the command" %
507 def FormatError(err):
508 """Return a formatted error message for a given error.
510 This function takes an exception instance and returns a tuple
511 consisting of two values: first, the recommended exit code, and
512 second, a string describing the error message (not
519 if isinstance(err, errors.ConfigurationError):
520 txt = "Corrupt configuration file: %s" % msg
522 obuf.write(txt + "\n")
523 obuf.write("Aborting.")
525 elif isinstance(err, errors.HooksAbort):
526 obuf.write("Failure: hooks execution failed:\n")
527 for node, script, out in err.args[0]:
529 obuf.write(" node: %s, script: %s, output: %s\n" %
532 obuf.write(" node: %s, script: %s (no output)\n" %
534 elif isinstance(err, errors.HooksFailure):
535 obuf.write("Failure: hooks general failure: %s" % msg)
536 elif isinstance(err, errors.ResolverError):
537 this_host = utils.HostInfo.SysName()
538 if err.args[0] == this_host:
539 msg = "Failure: can't resolve my own hostname ('%s')"
541 msg = "Failure: can't resolve hostname '%s'"
542 obuf.write(msg % err.args[0])
543 elif isinstance(err, errors.OpPrereqError):
544 obuf.write("Failure: prerequisites not met for this"
545 " operation:\n%s" % msg)
546 elif isinstance(err, errors.OpExecError):
547 obuf.write("Failure: command execution error:\n%s" % msg)
548 elif isinstance(err, errors.TagError):
549 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
550 elif isinstance(err, errors.GenericError):
551 obuf.write("Unhandled Ganeti error: %s" % msg)
552 elif isinstance(err, luxi.NoMasterError):
553 obuf.write("Cannot communicate with the master daemon.\nIs it running"
554 " and listening for connections?")
555 elif isinstance(err, luxi.TimeoutError):
556 obuf.write("Timeout while talking to the master daemon. Error:\n"
558 elif isinstance(err, luxi.ProtocolError):
559 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
561 elif isinstance(err, JobSubmittedException):
562 obuf.write("JobID: %s\n" % err.args[0])
565 obuf.write("Unhandled exception: %s" % msg)
566 return retcode, obuf.getvalue().rstrip('\n')
569 def GenericMain(commands, override=None, aliases=None):
570 """Generic main function for all the gnt-* commands.
573 - commands: a dictionary with a special structure, see the design doc
574 for command line handling.
575 - override: if not None, we expect a dictionary with keys that will
576 override command line options; this can be used to pass
577 options from the scripts to generic functions
578 - aliases: dictionary with command aliases {'alias': 'target, ...}
581 # save the program name and the entire command line for later logging
583 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
584 if len(sys.argv) >= 2:
585 binary += " " + sys.argv[1]
586 old_cmdline = " ".join(sys.argv[2:])
590 binary = "<unknown program>"
596 func, options, args = _ParseArgs(sys.argv, commands, aliases)
597 if func is None: # parse error
600 if override is not None:
601 for key, val in override.iteritems():
602 setattr(options, key, val)
604 logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
605 stderr_logging=True, program=binary)
607 utils.debug = options.debug
610 logger.Info("run with arguments '%s'" % old_cmdline)
612 logger.Info("run with no arguments")
615 result = func(options, args)
616 except (errors.GenericError, luxi.ProtocolError), err:
617 result, err_msg = FormatError(err)
618 logger.ToStderr(err_msg)
623 def GenerateTable(headers, fields, separator, data,
624 numfields=None, unitfields=None):
625 """Prints a table with headers and different fields.
628 headers: Dict of header titles or None if no headers should be shown
629 fields: List of fields to show
630 separator: String used to separate fields or None for spaces
631 data: Data to be printed
632 numfields: List of fields to be aligned to right
633 unitfields: List of fields to be formatted as units
636 if numfields is None:
638 if unitfields is None:
643 if headers and field not in headers:
644 raise errors.ProgrammerError("Missing header description for field '%s'"
646 if separator is not None:
647 format_fields.append("%s")
648 elif field in numfields:
649 format_fields.append("%*s")
651 format_fields.append("%-*s")
653 if separator is None:
654 mlens = [0 for name in fields]
655 format = ' '.join(format_fields)
657 format = separator.replace("%", "%%").join(format_fields)
660 for idx, val in enumerate(row):
661 if fields[idx] in unitfields:
667 val = row[idx] = utils.FormatUnit(val)
668 val = row[idx] = str(val)
669 if separator is None:
670 mlens[idx] = max(mlens[idx], len(val))
675 for idx, name in enumerate(fields):
677 if separator is None:
678 mlens[idx] = max(mlens[idx], len(hdr))
679 args.append(mlens[idx])
681 result.append(format % tuple(args))
685 for idx in xrange(len(fields)):
686 if separator is None:
687 args.append(mlens[idx])
688 args.append(line[idx])
689 result.append(format % tuple(args))
694 def FormatTimestamp(ts):
695 """Formats a given timestamp.
698 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
701 @returns: a string with the formatted timestamp
705 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec