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", "ikv_option", "keyval_option",
46 "GenerateTable", "AskUser",
47 "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
48 "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT", "SUBMIT_OPT",
49 "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
50 "FormatError", "SplitNodeOption", "SubmitOrSend",
51 "JobSubmittedException", "FormatTimestamp", "ParseTimespec",
55 def _ExtractTagsObject(opts, args):
56 """Extract the tag type object.
58 Note that this function will modify its args parameter.
61 if not hasattr(opts, "tag_type"):
62 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
64 if kind == constants.TAG_CLUSTER:
66 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
68 raise errors.OpPrereqError("no arguments passed to the command")
72 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
76 def _ExtendTags(opts, args):
77 """Extend the args if a source file has been given.
79 This function will extend the tags with the contents of the file
80 passed in the 'tags_source' attribute of the opts parameter. A file
81 named '-' will be replaced by stdin.
84 fname = opts.tags_source
90 new_fh = open(fname, "r")
93 # we don't use the nice 'new_data = [line.strip() for line in fh]'
94 # because of python bug 1633941
96 line = new_fh.readline()
99 new_data.append(line.strip())
102 args.extend(new_data)
105 def ListTags(opts, args):
106 """List the tags on a given object.
108 This is a generic implementation that knows how to deal with all
109 three cases of tag objects (cluster, node, instance). The opts
110 argument is expected to contain a tag_type field denoting what
111 object type we work on.
114 kind, name = _ExtractTagsObject(opts, args)
115 op = opcodes.OpGetTags(kind=kind, name=name)
116 result = SubmitOpCode(op)
117 result = list(result)
123 def AddTags(opts, args):
124 """Add tags on a given object.
126 This is a generic implementation that knows how to deal with all
127 three cases of tag objects (cluster, node, instance). The opts
128 argument is expected to contain a tag_type field denoting what
129 object type we work on.
132 kind, name = _ExtractTagsObject(opts, args)
133 _ExtendTags(opts, args)
135 raise errors.OpPrereqError("No tags to be added")
136 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
140 def RemoveTags(opts, args):
141 """Remove tags from a given object.
143 This is a generic implementation that knows how to deal with all
144 three cases of tag objects (cluster, node, instance). The opts
145 argument is expected to contain a tag_type field denoting what
146 object type we work on.
149 kind, name = _ExtractTagsObject(opts, args)
150 _ExtendTags(opts, args)
152 raise errors.OpPrereqError("No tags to be removed")
153 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
157 DEBUG_OPT = make_option("-d", "--debug", default=False,
159 help="Turn debugging on")
161 NOHDR_OPT = make_option("--no-headers", default=False,
162 action="store_true", dest="no_headers",
163 help="Don't display column headers")
165 SEP_OPT = make_option("--separator", default=None,
166 action="store", dest="separator",
167 help="Separator between output fields"
168 " (defaults to one space)")
170 USEUNITS_OPT = make_option("--human-readable", default=False,
171 action="store_true", dest="human_readable",
172 help="Print sizes in human readable format")
174 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
175 type="string", help="Comma separated list of"
179 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
180 default=False, help="Force the operation")
182 TAG_SRC_OPT = make_option("--from", dest="tags_source",
183 default=None, help="File with tag names")
185 SUBMIT_OPT = make_option("--submit", dest="submit_only",
186 default=False, action="store_true",
187 help="Submit the job and return the job ID, but"
188 " don't wait for the job to finish")
192 """Macro-like function denoting a fixed number of arguments"""
196 def ARGS_ATLEAST(val):
197 """Macro-like function denoting a minimum number of arguments"""
202 ARGS_ONE = ARGS_FIXED(1)
203 ARGS_ANY = ARGS_ATLEAST(0)
206 def check_unit(option, opt, value):
207 """OptParsers custom converter for units.
211 return utils.ParseUnit(value)
212 except errors.UnitParseError, err:
213 raise OptionValueError("option %s: %s" % (opt, err))
216 class CliOption(Option):
217 """Custom option class for optparse.
220 TYPES = Option.TYPES + ("unit",)
221 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
222 TYPE_CHECKER["unit"] = check_unit
225 def _SplitKeyVal(opt, data):
226 """Convert a KeyVal string into a dict.
228 This function will convert a key=val[,...] string into a dict. Empty
229 values will be converted specially: keys which have the prefix 'no_'
230 will have the value=False and the prefix stripped, the others will
234 @param opt: a string holding the option name for which we process the
235 data, used in building error messages
237 @param data: a string of the format key=val,key=val,...
239 @return: {key=val, key=val}
240 @raises errors.ParameterError: if there are duplicate keys
246 for elem in data.split(","):
248 key, val = elem.split("=", 1)
250 if elem.startswith(NO_PREFIX):
251 key, val = elem[len(NO_PREFIX):], False
252 elif elem.startswith(UN_PREFIX):
253 key, val = elem[len(UN_PREFIX):], None
255 key, val = elem, True
257 raise errors.ParameterError("Duplicate key '%s' in option %s" %
263 def check_ident_key_val(option, opt, value):
264 """Custom parser for the IdentKeyVal option type.
270 ident, rest = value.split(":", 1)
271 kv_dict = _SplitKeyVal(opt, rest)
272 retval = (ident, kv_dict)
276 class IdentKeyValOption(Option):
277 """Custom option class for ident:key=val,key=val options.
279 This will store the parsed values as a tuple (ident, {key: val}). As
280 such, multiple uses of this option via action=append is possible.
283 TYPES = Option.TYPES + ("identkeyval",)
284 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
285 TYPE_CHECKER["identkeyval"] = check_ident_key_val
288 def check_key_val(option, opt, value):
289 """Custom parser for the KeyVal option type.
292 return _SplitKeyVal(opt, value)
295 class KeyValOption(Option):
296 """Custom option class for key=val,key=val options.
298 This will store the parsed values as a dict {key: val}.
301 TYPES = Option.TYPES + ("keyval",)
302 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
303 TYPE_CHECKER["keyval"] = check_key_val
306 # optparse.py sets make_option, so we do it for our own option class, too
307 cli_option = CliOption
308 ikv_option = IdentKeyValOption
309 keyval_option = KeyValOption
312 def _ParseArgs(argv, commands, aliases):
313 """Parses the command line and return the function which must be
314 executed together with its arguments
317 argv: the command line
319 commands: dictionary with special contents, see the design doc for
321 aliases: dictionary with command aliases {'alias': 'target, ...}
327 binary = argv[0].split("/")[-1]
329 if len(argv) > 1 and argv[1] == "--version":
330 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
331 # Quit right away. That way we don't have to care about this special
332 # argument. optparse.py does it the same.
335 if len(argv) < 2 or not (argv[1] in commands or
337 # let's do a nice thing
338 sortedcmds = commands.keys()
340 print ("Usage: %(bin)s {command} [options...] [argument...]"
341 "\n%(bin)s <command> --help to see details, or"
342 " man %(bin)s\n" % {"bin": binary})
343 # compute the max line length for cmd + usage
344 mlen = max([len(" %s" % cmd) for cmd in commands])
345 mlen = min(60, mlen) # should not get here...
346 # and format a nice command list
348 for cmd in sortedcmds:
349 cmdstr = " %s" % (cmd,)
350 help_text = commands[cmd][4]
351 help_lines = textwrap.wrap(help_text, 79-3-mlen)
352 print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
353 for line in help_lines:
354 print "%-*s %s" % (mlen, "", line)
356 return None, None, None
358 # get command, unalias it, and look it up in commands
362 raise errors.ProgrammerError("Alias '%s' overrides an existing"
365 if aliases[cmd] not in commands:
366 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
367 " command '%s'" % (cmd, aliases[cmd]))
371 func, nargs, parser_opts, usage, description = commands[cmd]
372 parser = OptionParser(option_list=parser_opts,
373 description=description,
374 formatter=TitledHelpFormatter(),
375 usage="%%prog %s %s" % (cmd, usage))
376 parser.disable_interspersed_args()
377 options, args = parser.parse_args()
380 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
381 return None, None, None
382 elif nargs < 0 and len(args) != -nargs:
383 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
385 return None, None, None
386 elif nargs >= 0 and len(args) < nargs:
387 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
389 return None, None, None
391 return func, options, args
394 def SplitNodeOption(value):
395 """Splits the value of a --node option.
398 if value and ':' in value:
399 return value.split(':', 1)
404 def AskUser(text, choices=None):
405 """Ask the user a question.
408 text - the question to ask.
410 choices - list with elements tuples (input_char, return_value,
411 description); if not given, it will default to: [('y', True,
412 'Perform the operation'), ('n', False, 'Do no do the operation')];
413 note that the '?' char is reserved for help
415 Returns: one of the return values from the choices list; if input is
416 not possible (i.e. not running with a tty, we return the last entry
421 choices = [('y', True, 'Perform the operation'),
422 ('n', False, 'Do not perform the operation')]
423 if not choices or not isinstance(choices, list):
424 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
425 for entry in choices:
426 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
427 raise errors.ProgrammerError("Invalid choiches element to AskUser")
429 answer = choices[-1][1]
431 for line in text.splitlines():
432 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
433 text = "\n".join(new_text)
435 f = file("/dev/tty", "a+")
439 chars = [entry[0] for entry in choices]
440 chars[-1] = "[%s]" % chars[-1]
442 maps = dict([(entry[0], entry[1]) for entry in choices])
446 f.write("/".join(chars))
448 line = f.readline(2).strip().lower()
453 for entry in choices:
454 f.write(" %s - %s\n" % (entry[0], entry[2]))
462 class JobSubmittedException(Exception):
463 """Job was submitted, client should exit.
465 This exception has one argument, the ID of the job that was
466 submitted. The handler should print this ID.
468 This is not an error, just a structured way to exit from clients.
473 def SendJob(ops, cl=None):
474 """Function to submit an opcode without waiting for the results.
477 @param ops: list of opcodes
478 @type cl: luxi.Client
479 @param cl: the luxi client to use for communicating with the master;
480 if None, a new client will be created
486 job_id = cl.SubmitJob(ops)
491 def PollJob(job_id, cl=None, feedback_fn=None):
492 """Function to poll for the result of a job.
494 @type job_id: job identified
495 @param job_id: the job to poll for results
496 @type cl: luxi.Client
497 @param cl: the luxi client to use for communicating with the master;
498 if None, a new client will be created
505 prev_logmsg_serial = None
508 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
511 # job not found, go away!
512 raise errors.JobLost("Job with id %s lost" % job_id)
514 # Split result, a tuple of (field values, log entries)
515 (job_info, log_entries) = result
516 (status, ) = job_info
519 for log_entry in log_entries:
520 (serial, timestamp, _, message) = log_entry
521 if callable(feedback_fn):
522 feedback_fn(log_entry[1:])
524 print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), message)
525 prev_logmsg_serial = max(prev_logmsg_serial, serial)
527 # TODO: Handle canceled and archived jobs
528 elif status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
531 prev_job_info = job_info
533 jobs = cl.QueryJobs([job_id], ["status", "opresult"])
535 raise errors.JobLost("Job with id %s lost" % job_id)
537 status, result = jobs[0]
538 if status == constants.JOB_STATUS_SUCCESS:
541 raise errors.OpExecError(result)
544 def SubmitOpCode(op, cl=None, feedback_fn=None):
545 """Legacy function to submit an opcode.
547 This is just a simple wrapper over the construction of the processor
548 instance. It should be extended to better handle feedback and
549 interaction functions.
555 job_id = SendJob([op], cl)
557 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
562 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
563 """Wrapper around SubmitOpCode or SendJob.
565 This function will decide, based on the 'opts' parameter, whether to
566 submit and wait for the result of the opcode (and return it), or
567 whether to just send the job and print its identifier. It is used in
568 order to simplify the implementation of the '--submit' option.
571 if opts and opts.submit_only:
572 job_id = SendJob([op], cl=cl)
573 raise JobSubmittedException(job_id)
575 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
579 # TODO: Cache object?
581 client = luxi.Client()
582 except luxi.NoMasterError:
583 master, myself = ssconf.GetMasterAndMyself()
585 raise errors.OpPrereqError("This is not the master node, please connect"
586 " to node '%s' and rerun the command" %
593 def FormatError(err):
594 """Return a formatted error message for a given error.
596 This function takes an exception instance and returns a tuple
597 consisting of two values: first, the recommended exit code, and
598 second, a string describing the error message (not
605 if isinstance(err, errors.ConfigurationError):
606 txt = "Corrupt configuration file: %s" % msg
608 obuf.write(txt + "\n")
609 obuf.write("Aborting.")
611 elif isinstance(err, errors.HooksAbort):
612 obuf.write("Failure: hooks execution failed:\n")
613 for node, script, out in err.args[0]:
615 obuf.write(" node: %s, script: %s, output: %s\n" %
618 obuf.write(" node: %s, script: %s (no output)\n" %
620 elif isinstance(err, errors.HooksFailure):
621 obuf.write("Failure: hooks general failure: %s" % msg)
622 elif isinstance(err, errors.ResolverError):
623 this_host = utils.HostInfo.SysName()
624 if err.args[0] == this_host:
625 msg = "Failure: can't resolve my own hostname ('%s')"
627 msg = "Failure: can't resolve hostname '%s'"
628 obuf.write(msg % err.args[0])
629 elif isinstance(err, errors.OpPrereqError):
630 obuf.write("Failure: prerequisites not met for this"
631 " operation:\n%s" % msg)
632 elif isinstance(err, errors.OpExecError):
633 obuf.write("Failure: command execution error:\n%s" % msg)
634 elif isinstance(err, errors.TagError):
635 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
636 elif isinstance(err, errors.GenericError):
637 obuf.write("Unhandled Ganeti error: %s" % msg)
638 elif isinstance(err, luxi.NoMasterError):
639 obuf.write("Cannot communicate with the master daemon.\nIs it running"
640 " and listening for connections?")
641 elif isinstance(err, luxi.TimeoutError):
642 obuf.write("Timeout while talking to the master daemon. Error:\n"
644 elif isinstance(err, luxi.ProtocolError):
645 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
647 elif isinstance(err, JobSubmittedException):
648 obuf.write("JobID: %s\n" % err.args[0])
651 obuf.write("Unhandled exception: %s" % msg)
652 return retcode, obuf.getvalue().rstrip('\n')
655 def GenericMain(commands, override=None, aliases=None):
656 """Generic main function for all the gnt-* commands.
659 - commands: a dictionary with a special structure, see the design doc
660 for command line handling.
661 - override: if not None, we expect a dictionary with keys that will
662 override command line options; this can be used to pass
663 options from the scripts to generic functions
664 - aliases: dictionary with command aliases {'alias': 'target, ...}
667 # save the program name and the entire command line for later logging
669 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
670 if len(sys.argv) >= 2:
671 binary += " " + sys.argv[1]
672 old_cmdline = " ".join(sys.argv[2:])
676 binary = "<unknown program>"
682 func, options, args = _ParseArgs(sys.argv, commands, aliases)
683 if func is None: # parse error
686 if override is not None:
687 for key, val in override.iteritems():
688 setattr(options, key, val)
690 logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
691 stderr_logging=True, program=binary)
693 utils.debug = options.debug
696 logger.Info("run with arguments '%s'" % old_cmdline)
698 logger.Info("run with no arguments")
701 result = func(options, args)
702 except (errors.GenericError, luxi.ProtocolError), err:
703 result, err_msg = FormatError(err)
704 logger.ToStderr(err_msg)
709 def GenerateTable(headers, fields, separator, data,
710 numfields=None, unitfields=None):
711 """Prints a table with headers and different fields.
714 headers: Dict of header titles or None if no headers should be shown
715 fields: List of fields to show
716 separator: String used to separate fields or None for spaces
717 data: Data to be printed
718 numfields: List of fields to be aligned to right
719 unitfields: List of fields to be formatted as units
722 if numfields is None:
724 if unitfields is None:
729 if headers and field not in headers:
730 raise errors.ProgrammerError("Missing header description for field '%s'"
732 if separator is not None:
733 format_fields.append("%s")
734 elif field in numfields:
735 format_fields.append("%*s")
737 format_fields.append("%-*s")
739 if separator is None:
740 mlens = [0 for name in fields]
741 format = ' '.join(format_fields)
743 format = separator.replace("%", "%%").join(format_fields)
746 for idx, val in enumerate(row):
747 if fields[idx] in unitfields:
753 val = row[idx] = utils.FormatUnit(val)
754 val = row[idx] = str(val)
755 if separator is None:
756 mlens[idx] = max(mlens[idx], len(val))
761 for idx, name in enumerate(fields):
763 if separator is None:
764 mlens[idx] = max(mlens[idx], len(hdr))
765 args.append(mlens[idx])
767 result.append(format % tuple(args))
771 for idx in xrange(len(fields)):
772 if separator is None:
773 args.append(mlens[idx])
774 args.append(line[idx])
775 result.append(format % tuple(args))
780 def FormatTimestamp(ts):
781 """Formats a given timestamp.
784 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
787 @returns: a string with the formatted timestamp
790 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
793 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
796 def ParseTimespec(value):
797 """Parse a time specification.
799 The following suffixed will be recognized:
807 Without any suffix, the value will be taken to be in seconds.
812 raise errors.OpPrereqError("Empty time specification passed")
820 if value[-1] not in suffix_map:
824 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
826 multiplier = suffix_map[value[-1]]
828 if not value: # no data left after stripping the suffix
829 raise errors.OpPrereqError("Invalid time specification (only"
832 value = int(value) * multiplier
834 raise errors.OpPrereqError("Invalid time specification '%s'" % value)