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",
56 def _ExtractTagsObject(opts, args):
57 """Extract the tag type object.
59 Note that this function will modify its args parameter.
62 if not hasattr(opts, "tag_type"):
63 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
65 if kind == constants.TAG_CLUSTER:
67 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
69 raise errors.OpPrereqError("no arguments passed to the command")
73 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
77 def _ExtendTags(opts, args):
78 """Extend the args if a source file has been given.
80 This function will extend the tags with the contents of the file
81 passed in the 'tags_source' attribute of the opts parameter. A file
82 named '-' will be replaced by stdin.
85 fname = opts.tags_source
91 new_fh = open(fname, "r")
94 # we don't use the nice 'new_data = [line.strip() for line in fh]'
95 # because of python bug 1633941
97 line = new_fh.readline()
100 new_data.append(line.strip())
103 args.extend(new_data)
106 def ListTags(opts, args):
107 """List the tags on a given object.
109 This is a generic implementation that knows how to deal with all
110 three cases of tag objects (cluster, node, instance). The opts
111 argument is expected to contain a tag_type field denoting what
112 object type we work on.
115 kind, name = _ExtractTagsObject(opts, args)
116 op = opcodes.OpGetTags(kind=kind, name=name)
117 result = SubmitOpCode(op)
118 result = list(result)
124 def AddTags(opts, args):
125 """Add tags on a given object.
127 This is a generic implementation that knows how to deal with all
128 three cases of tag objects (cluster, node, instance). The opts
129 argument is expected to contain a tag_type field denoting what
130 object type we work on.
133 kind, name = _ExtractTagsObject(opts, args)
134 _ExtendTags(opts, args)
136 raise errors.OpPrereqError("No tags to be added")
137 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
141 def RemoveTags(opts, args):
142 """Remove tags from a given object.
144 This is a generic implementation that knows how to deal with all
145 three cases of tag objects (cluster, node, instance). The opts
146 argument is expected to contain a tag_type field denoting what
147 object type we work on.
150 kind, name = _ExtractTagsObject(opts, args)
151 _ExtendTags(opts, args)
153 raise errors.OpPrereqError("No tags to be removed")
154 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
158 DEBUG_OPT = make_option("-d", "--debug", default=False,
160 help="Turn debugging on")
162 NOHDR_OPT = make_option("--no-headers", default=False,
163 action="store_true", dest="no_headers",
164 help="Don't display column headers")
166 SEP_OPT = make_option("--separator", default=None,
167 action="store", dest="separator",
168 help="Separator between output fields"
169 " (defaults to one space)")
171 USEUNITS_OPT = make_option("--human-readable", default=False,
172 action="store_true", dest="human_readable",
173 help="Print sizes in human readable format")
175 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
176 type="string", help="Comma separated list of"
180 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
181 default=False, help="Force the operation")
183 TAG_SRC_OPT = make_option("--from", dest="tags_source",
184 default=None, help="File with tag names")
186 SUBMIT_OPT = make_option("--submit", dest="submit_only",
187 default=False, action="store_true",
188 help="Submit the job and return the job ID, but"
189 " don't wait for the job to finish")
193 """Macro-like function denoting a fixed number of arguments"""
197 def ARGS_ATLEAST(val):
198 """Macro-like function denoting a minimum number of arguments"""
203 ARGS_ONE = ARGS_FIXED(1)
204 ARGS_ANY = ARGS_ATLEAST(0)
207 def check_unit(option, opt, value):
208 """OptParsers custom converter for units.
212 return utils.ParseUnit(value)
213 except errors.UnitParseError, err:
214 raise OptionValueError("option %s: %s" % (opt, err))
217 class CliOption(Option):
218 """Custom option class for optparse.
221 TYPES = Option.TYPES + ("unit",)
222 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
223 TYPE_CHECKER["unit"] = check_unit
226 def _SplitKeyVal(opt, data):
227 """Convert a KeyVal string into a dict.
229 This function will convert a key=val[,...] string into a dict. Empty
230 values will be converted specially: keys which have the prefix 'no_'
231 will have the value=False and the prefix stripped, the others will
235 @param opt: a string holding the option name for which we process the
236 data, used in building error messages
238 @param data: a string of the format key=val,key=val,...
240 @return: {key=val, key=val}
241 @raises errors.ParameterError: if there are duplicate keys
247 for elem in data.split(","):
249 key, val = elem.split("=", 1)
251 if elem.startswith(NO_PREFIX):
252 key, val = elem[len(NO_PREFIX):], False
253 elif elem.startswith(UN_PREFIX):
254 key, val = elem[len(UN_PREFIX):], None
256 key, val = elem, True
258 raise errors.ParameterError("Duplicate key '%s' in option %s" %
264 def check_ident_key_val(option, opt, value):
265 """Custom parser for the IdentKeyVal option type.
271 ident, rest = value.split(":", 1)
272 kv_dict = _SplitKeyVal(opt, rest)
273 retval = (ident, kv_dict)
277 class IdentKeyValOption(Option):
278 """Custom option class for ident:key=val,key=val options.
280 This will store the parsed values as a tuple (ident, {key: val}). As
281 such, multiple uses of this option via action=append is possible.
284 TYPES = Option.TYPES + ("identkeyval",)
285 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
286 TYPE_CHECKER["identkeyval"] = check_ident_key_val
289 def check_key_val(option, opt, value):
290 """Custom parser for the KeyVal option type.
293 return _SplitKeyVal(opt, value)
296 class KeyValOption(Option):
297 """Custom option class for key=val,key=val options.
299 This will store the parsed values as a dict {key: val}.
302 TYPES = Option.TYPES + ("keyval",)
303 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
304 TYPE_CHECKER["keyval"] = check_key_val
307 # optparse.py sets make_option, so we do it for our own option class, too
308 cli_option = CliOption
309 ikv_option = IdentKeyValOption
310 keyval_option = KeyValOption
313 def _ParseArgs(argv, commands, aliases):
314 """Parses the command line and return the function which must be
315 executed together with its arguments
318 argv: the command line
320 commands: dictionary with special contents, see the design doc for
322 aliases: dictionary with command aliases {'alias': 'target, ...}
328 binary = argv[0].split("/")[-1]
330 if len(argv) > 1 and argv[1] == "--version":
331 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
332 # Quit right away. That way we don't have to care about this special
333 # argument. optparse.py does it the same.
336 if len(argv) < 2 or not (argv[1] in commands or
338 # let's do a nice thing
339 sortedcmds = commands.keys()
341 print ("Usage: %(bin)s {command} [options...] [argument...]"
342 "\n%(bin)s <command> --help to see details, or"
343 " man %(bin)s\n" % {"bin": binary})
344 # compute the max line length for cmd + usage
345 mlen = max([len(" %s" % cmd) for cmd in commands])
346 mlen = min(60, mlen) # should not get here...
347 # and format a nice command list
349 for cmd in sortedcmds:
350 cmdstr = " %s" % (cmd,)
351 help_text = commands[cmd][4]
352 help_lines = textwrap.wrap(help_text, 79-3-mlen)
353 print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
354 for line in help_lines:
355 print "%-*s %s" % (mlen, "", line)
357 return None, None, None
359 # get command, unalias it, and look it up in commands
363 raise errors.ProgrammerError("Alias '%s' overrides an existing"
366 if aliases[cmd] not in commands:
367 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
368 " command '%s'" % (cmd, aliases[cmd]))
372 func, nargs, parser_opts, usage, description = commands[cmd]
373 parser = OptionParser(option_list=parser_opts,
374 description=description,
375 formatter=TitledHelpFormatter(),
376 usage="%%prog %s %s" % (cmd, usage))
377 parser.disable_interspersed_args()
378 options, args = parser.parse_args()
381 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
382 return None, None, None
383 elif nargs < 0 and len(args) != -nargs:
384 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
386 return None, None, None
387 elif nargs >= 0 and len(args) < nargs:
388 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
390 return None, None, None
392 return func, options, args
395 def SplitNodeOption(value):
396 """Splits the value of a --node option.
399 if value and ':' in value:
400 return value.split(':', 1)
405 def ValidateBeParams(bep):
406 """Parse and check the given beparams.
408 The function will update in-place the given dictionary.
411 @param bep: input beparams
412 @raise errors.ParameterError: if the input values are not OK
413 @raise errors.UnitParseError: if the input values are not OK
416 if constants.BE_MEMORY in bep:
417 bep[constants.BE_MEMORY] = utils.ParseUnit(bep[constants.BE_MEMORY])
419 if constants.BE_VCPUS in bep:
421 bep[constants.BE_VCPUS] = int(bep[constants.BE_VCPUS])
423 raise errors.ParameterError("Invalid number of VCPUs")
426 def AskUser(text, choices=None):
427 """Ask the user a question.
430 text - the question to ask.
432 choices - list with elements tuples (input_char, return_value,
433 description); if not given, it will default to: [('y', True,
434 'Perform the operation'), ('n', False, 'Do no do the operation')];
435 note that the '?' char is reserved for help
437 Returns: one of the return values from the choices list; if input is
438 not possible (i.e. not running with a tty, we return the last entry
443 choices = [('y', True, 'Perform the operation'),
444 ('n', False, 'Do not perform the operation')]
445 if not choices or not isinstance(choices, list):
446 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
447 for entry in choices:
448 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
449 raise errors.ProgrammerError("Invalid choiches element to AskUser")
451 answer = choices[-1][1]
453 for line in text.splitlines():
454 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
455 text = "\n".join(new_text)
457 f = file("/dev/tty", "a+")
461 chars = [entry[0] for entry in choices]
462 chars[-1] = "[%s]" % chars[-1]
464 maps = dict([(entry[0], entry[1]) for entry in choices])
468 f.write("/".join(chars))
470 line = f.readline(2).strip().lower()
475 for entry in choices:
476 f.write(" %s - %s\n" % (entry[0], entry[2]))
484 class JobSubmittedException(Exception):
485 """Job was submitted, client should exit.
487 This exception has one argument, the ID of the job that was
488 submitted. The handler should print this ID.
490 This is not an error, just a structured way to exit from clients.
495 def SendJob(ops, cl=None):
496 """Function to submit an opcode without waiting for the results.
499 @param ops: list of opcodes
500 @type cl: luxi.Client
501 @param cl: the luxi client to use for communicating with the master;
502 if None, a new client will be created
508 job_id = cl.SubmitJob(ops)
513 def PollJob(job_id, cl=None, feedback_fn=None):
514 """Function to poll for the result of a job.
516 @type job_id: job identified
517 @param job_id: the job to poll for results
518 @type cl: luxi.Client
519 @param cl: the luxi client to use for communicating with the master;
520 if None, a new client will be created
527 prev_logmsg_serial = None
530 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
533 # job not found, go away!
534 raise errors.JobLost("Job with id %s lost" % job_id)
536 # Split result, a tuple of (field values, log entries)
537 (job_info, log_entries) = result
538 (status, ) = job_info
541 for log_entry in log_entries:
542 (serial, timestamp, _, message) = log_entry
543 if callable(feedback_fn):
544 feedback_fn(log_entry[1:])
546 print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), message)
547 prev_logmsg_serial = max(prev_logmsg_serial, serial)
549 # TODO: Handle canceled and archived jobs
550 elif status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
553 prev_job_info = job_info
555 jobs = cl.QueryJobs([job_id], ["status", "opresult"])
557 raise errors.JobLost("Job with id %s lost" % job_id)
559 status, result = jobs[0]
560 if status == constants.JOB_STATUS_SUCCESS:
563 raise errors.OpExecError(result)
566 def SubmitOpCode(op, cl=None, feedback_fn=None):
567 """Legacy function to submit an opcode.
569 This is just a simple wrapper over the construction of the processor
570 instance. It should be extended to better handle feedback and
571 interaction functions.
577 job_id = SendJob([op], cl)
579 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
584 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
585 """Wrapper around SubmitOpCode or SendJob.
587 This function will decide, based on the 'opts' parameter, whether to
588 submit and wait for the result of the opcode (and return it), or
589 whether to just send the job and print its identifier. It is used in
590 order to simplify the implementation of the '--submit' option.
593 if opts and opts.submit_only:
594 job_id = SendJob([op], cl=cl)
595 raise JobSubmittedException(job_id)
597 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
601 # TODO: Cache object?
603 client = luxi.Client()
604 except luxi.NoMasterError:
605 master, myself = ssconf.GetMasterAndMyself()
607 raise errors.OpPrereqError("This is not the master node, please connect"
608 " to node '%s' and rerun the command" %
615 def FormatError(err):
616 """Return a formatted error message for a given error.
618 This function takes an exception instance and returns a tuple
619 consisting of two values: first, the recommended exit code, and
620 second, a string describing the error message (not
627 if isinstance(err, errors.ConfigurationError):
628 txt = "Corrupt configuration file: %s" % msg
630 obuf.write(txt + "\n")
631 obuf.write("Aborting.")
633 elif isinstance(err, errors.HooksAbort):
634 obuf.write("Failure: hooks execution failed:\n")
635 for node, script, out in err.args[0]:
637 obuf.write(" node: %s, script: %s, output: %s\n" %
640 obuf.write(" node: %s, script: %s (no output)\n" %
642 elif isinstance(err, errors.HooksFailure):
643 obuf.write("Failure: hooks general failure: %s" % msg)
644 elif isinstance(err, errors.ResolverError):
645 this_host = utils.HostInfo.SysName()
646 if err.args[0] == this_host:
647 msg = "Failure: can't resolve my own hostname ('%s')"
649 msg = "Failure: can't resolve hostname '%s'"
650 obuf.write(msg % err.args[0])
651 elif isinstance(err, errors.OpPrereqError):
652 obuf.write("Failure: prerequisites not met for this"
653 " operation:\n%s" % msg)
654 elif isinstance(err, errors.OpExecError):
655 obuf.write("Failure: command execution error:\n%s" % msg)
656 elif isinstance(err, errors.TagError):
657 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
658 elif isinstance(err, errors.GenericError):
659 obuf.write("Unhandled Ganeti error: %s" % msg)
660 elif isinstance(err, luxi.NoMasterError):
661 obuf.write("Cannot communicate with the master daemon.\nIs it running"
662 " and listening for connections?")
663 elif isinstance(err, luxi.TimeoutError):
664 obuf.write("Timeout while talking to the master daemon. Error:\n"
666 elif isinstance(err, luxi.ProtocolError):
667 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
669 elif isinstance(err, JobSubmittedException):
670 obuf.write("JobID: %s\n" % err.args[0])
673 obuf.write("Unhandled exception: %s" % msg)
674 return retcode, obuf.getvalue().rstrip('\n')
677 def GenericMain(commands, override=None, aliases=None):
678 """Generic main function for all the gnt-* commands.
681 - commands: a dictionary with a special structure, see the design doc
682 for command line handling.
683 - override: if not None, we expect a dictionary with keys that will
684 override command line options; this can be used to pass
685 options from the scripts to generic functions
686 - aliases: dictionary with command aliases {'alias': 'target, ...}
689 # save the program name and the entire command line for later logging
691 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
692 if len(sys.argv) >= 2:
693 binary += " " + sys.argv[1]
694 old_cmdline = " ".join(sys.argv[2:])
698 binary = "<unknown program>"
704 func, options, args = _ParseArgs(sys.argv, commands, aliases)
705 if func is None: # parse error
708 if override is not None:
709 for key, val in override.iteritems():
710 setattr(options, key, val)
712 logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
713 stderr_logging=True, program=binary)
715 utils.debug = options.debug
718 logger.Info("run with arguments '%s'" % old_cmdline)
720 logger.Info("run with no arguments")
723 result = func(options, args)
724 except (errors.GenericError, luxi.ProtocolError), err:
725 result, err_msg = FormatError(err)
726 logger.ToStderr(err_msg)
731 def GenerateTable(headers, fields, separator, data,
732 numfields=None, unitfields=None):
733 """Prints a table with headers and different fields.
736 headers: Dict of header titles or None if no headers should be shown
737 fields: List of fields to show
738 separator: String used to separate fields or None for spaces
739 data: Data to be printed
740 numfields: List of fields to be aligned to right
741 unitfields: List of fields to be formatted as units
744 if numfields is None:
746 if unitfields is None:
751 if headers and field not in headers:
752 raise errors.ProgrammerError("Missing header description for field '%s'"
754 if separator is not None:
755 format_fields.append("%s")
756 elif field in numfields:
757 format_fields.append("%*s")
759 format_fields.append("%-*s")
761 if separator is None:
762 mlens = [0 for name in fields]
763 format = ' '.join(format_fields)
765 format = separator.replace("%", "%%").join(format_fields)
768 for idx, val in enumerate(row):
769 if fields[idx] in unitfields:
775 val = row[idx] = utils.FormatUnit(val)
776 val = row[idx] = str(val)
777 if separator is None:
778 mlens[idx] = max(mlens[idx], len(val))
783 for idx, name in enumerate(fields):
785 if separator is None:
786 mlens[idx] = max(mlens[idx], len(hdr))
787 args.append(mlens[idx])
789 result.append(format % tuple(args))
793 for idx in xrange(len(fields)):
794 if separator is None:
795 args.append(mlens[idx])
796 args.append(line[idx])
797 result.append(format % tuple(args))
802 def FormatTimestamp(ts):
803 """Formats a given timestamp.
806 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
809 @returns: a string with the formatted timestamp
812 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
815 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
818 def ParseTimespec(value):
819 """Parse a time specification.
821 The following suffixed will be recognized:
829 Without any suffix, the value will be taken to be in seconds.
834 raise errors.OpPrereqError("Empty time specification passed")
842 if value[-1] not in suffix_map:
846 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
848 multiplier = suffix_map[value[-1]]
850 if not value: # no data left after stripping the suffix
851 raise errors.OpPrereqError("Invalid time specification (only"
854 value = int(value) * multiplier
856 raise errors.OpPrereqError("Invalid time specification '%s'" % value)