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"""
31 from cStringIO import StringIO
33 from ganeti import utils
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",
53 "ToStderr", "ToStdout",
57 def _ExtractTagsObject(opts, args):
58 """Extract the tag type object.
60 Note that this function will modify its args parameter.
63 if not hasattr(opts, "tag_type"):
64 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
66 if kind == constants.TAG_CLUSTER:
68 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
70 raise errors.OpPrereqError("no arguments passed to the command")
74 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
78 def _ExtendTags(opts, args):
79 """Extend the args if a source file has been given.
81 This function will extend the tags with the contents of the file
82 passed in the 'tags_source' attribute of the opts parameter. A file
83 named '-' will be replaced by stdin.
86 fname = opts.tags_source
92 new_fh = open(fname, "r")
95 # we don't use the nice 'new_data = [line.strip() for line in fh]'
96 # because of python bug 1633941
98 line = new_fh.readline()
101 new_data.append(line.strip())
104 args.extend(new_data)
107 def ListTags(opts, args):
108 """List the tags on a given object.
110 This is a generic implementation that knows how to deal with all
111 three cases of tag objects (cluster, node, instance). The opts
112 argument is expected to contain a tag_type field denoting what
113 object type we work on.
116 kind, name = _ExtractTagsObject(opts, args)
117 op = opcodes.OpGetTags(kind=kind, name=name)
118 result = SubmitOpCode(op)
119 result = list(result)
125 def AddTags(opts, args):
126 """Add tags on a given object.
128 This is a generic implementation that knows how to deal with all
129 three cases of tag objects (cluster, node, instance). The opts
130 argument is expected to contain a tag_type field denoting what
131 object type we work on.
134 kind, name = _ExtractTagsObject(opts, args)
135 _ExtendTags(opts, args)
137 raise errors.OpPrereqError("No tags to be added")
138 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
142 def RemoveTags(opts, args):
143 """Remove tags from a given object.
145 This is a generic implementation that knows how to deal with all
146 three cases of tag objects (cluster, node, instance). The opts
147 argument is expected to contain a tag_type field denoting what
148 object type we work on.
151 kind, name = _ExtractTagsObject(opts, args)
152 _ExtendTags(opts, args)
154 raise errors.OpPrereqError("No tags to be removed")
155 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
159 DEBUG_OPT = make_option("-d", "--debug", default=False,
161 help="Turn debugging on")
163 NOHDR_OPT = make_option("--no-headers", default=False,
164 action="store_true", dest="no_headers",
165 help="Don't display column headers")
167 SEP_OPT = make_option("--separator", default=None,
168 action="store", dest="separator",
169 help="Separator between output fields"
170 " (defaults to one space)")
172 USEUNITS_OPT = make_option("--human-readable", default=False,
173 action="store_true", dest="human_readable",
174 help="Print sizes in human readable format")
176 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
177 type="string", help="Comma separated list of"
181 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
182 default=False, help="Force the operation")
184 TAG_SRC_OPT = make_option("--from", dest="tags_source",
185 default=None, help="File with tag names")
187 SUBMIT_OPT = make_option("--submit", dest="submit_only",
188 default=False, action="store_true",
189 help="Submit the job and return the job ID, but"
190 " don't wait for the job to finish")
194 """Macro-like function denoting a fixed number of arguments"""
198 def ARGS_ATLEAST(val):
199 """Macro-like function denoting a minimum number of arguments"""
204 ARGS_ONE = ARGS_FIXED(1)
205 ARGS_ANY = ARGS_ATLEAST(0)
208 def check_unit(option, opt, value):
209 """OptParsers custom converter for units.
213 return utils.ParseUnit(value)
214 except errors.UnitParseError, err:
215 raise OptionValueError("option %s: %s" % (opt, err))
218 class CliOption(Option):
219 """Custom option class for optparse.
222 TYPES = Option.TYPES + ("unit",)
223 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
224 TYPE_CHECKER["unit"] = check_unit
227 def _SplitKeyVal(opt, data):
228 """Convert a KeyVal string into a dict.
230 This function will convert a key=val[,...] string into a dict. Empty
231 values will be converted specially: keys which have the prefix 'no_'
232 will have the value=False and the prefix stripped, the others will
236 @param opt: a string holding the option name for which we process the
237 data, used in building error messages
239 @param data: a string of the format key=val,key=val,...
241 @return: {key=val, key=val}
242 @raises errors.ParameterError: if there are duplicate keys
248 for elem in data.split(","):
250 key, val = elem.split("=", 1)
252 if elem.startswith(NO_PREFIX):
253 key, val = elem[len(NO_PREFIX):], False
254 elif elem.startswith(UN_PREFIX):
255 key, val = elem[len(UN_PREFIX):], None
257 key, val = elem, True
259 raise errors.ParameterError("Duplicate key '%s' in option %s" %
265 def check_ident_key_val(option, opt, value):
266 """Custom parser for the IdentKeyVal option type.
272 ident, rest = value.split(":", 1)
273 kv_dict = _SplitKeyVal(opt, rest)
274 retval = (ident, kv_dict)
278 class IdentKeyValOption(Option):
279 """Custom option class for ident:key=val,key=val options.
281 This will store the parsed values as a tuple (ident, {key: val}). As
282 such, multiple uses of this option via action=append is possible.
285 TYPES = Option.TYPES + ("identkeyval",)
286 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
287 TYPE_CHECKER["identkeyval"] = check_ident_key_val
290 def check_key_val(option, opt, value):
291 """Custom parser for the KeyVal option type.
294 return _SplitKeyVal(opt, value)
297 class KeyValOption(Option):
298 """Custom option class for key=val,key=val options.
300 This will store the parsed values as a dict {key: val}.
303 TYPES = Option.TYPES + ("keyval",)
304 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
305 TYPE_CHECKER["keyval"] = check_key_val
308 # optparse.py sets make_option, so we do it for our own option class, too
309 cli_option = CliOption
310 ikv_option = IdentKeyValOption
311 keyval_option = KeyValOption
314 def _ParseArgs(argv, commands, aliases):
315 """Parses the command line and return the function which must be
316 executed together with its arguments
319 argv: the command line
321 commands: dictionary with special contents, see the design doc for
323 aliases: dictionary with command aliases {'alias': 'target, ...}
329 binary = argv[0].split("/")[-1]
331 if len(argv) > 1 and argv[1] == "--version":
332 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
333 # Quit right away. That way we don't have to care about this special
334 # argument. optparse.py does it the same.
337 if len(argv) < 2 or not (argv[1] in commands or
339 # let's do a nice thing
340 sortedcmds = commands.keys()
342 print ("Usage: %(bin)s {command} [options...] [argument...]"
343 "\n%(bin)s <command> --help to see details, or"
344 " man %(bin)s\n" % {"bin": binary})
345 # compute the max line length for cmd + usage
346 mlen = max([len(" %s" % cmd) for cmd in commands])
347 mlen = min(60, mlen) # should not get here...
348 # and format a nice command list
350 for cmd in sortedcmds:
351 cmdstr = " %s" % (cmd,)
352 help_text = commands[cmd][4]
353 help_lines = textwrap.wrap(help_text, 79-3-mlen)
354 print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
355 for line in help_lines:
356 print "%-*s %s" % (mlen, "", line)
358 return None, None, None
360 # get command, unalias it, and look it up in commands
364 raise errors.ProgrammerError("Alias '%s' overrides an existing"
367 if aliases[cmd] not in commands:
368 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
369 " command '%s'" % (cmd, aliases[cmd]))
373 func, nargs, parser_opts, usage, description = commands[cmd]
374 parser = OptionParser(option_list=parser_opts,
375 description=description,
376 formatter=TitledHelpFormatter(),
377 usage="%%prog %s %s" % (cmd, usage))
378 parser.disable_interspersed_args()
379 options, args = parser.parse_args()
382 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
383 return None, None, None
384 elif nargs < 0 and len(args) != -nargs:
385 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
387 return None, None, None
388 elif nargs >= 0 and len(args) < nargs:
389 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
391 return None, None, None
393 return func, options, args
396 def SplitNodeOption(value):
397 """Splits the value of a --node option.
400 if value and ':' in value:
401 return value.split(':', 1)
406 def ValidateBeParams(bep):
407 """Parse and check the given beparams.
409 The function will update in-place the given dictionary.
412 @param bep: input beparams
413 @raise errors.ParameterError: if the input values are not OK
414 @raise errors.UnitParseError: if the input values are not OK
417 if constants.BE_MEMORY in bep:
418 bep[constants.BE_MEMORY] = utils.ParseUnit(bep[constants.BE_MEMORY])
420 if constants.BE_VCPUS in bep:
422 bep[constants.BE_VCPUS] = int(bep[constants.BE_VCPUS])
424 raise errors.ParameterError("Invalid number of VCPUs")
427 def AskUser(text, choices=None):
428 """Ask the user a question.
431 text - the question to ask.
433 choices - list with elements tuples (input_char, return_value,
434 description); if not given, it will default to: [('y', True,
435 'Perform the operation'), ('n', False, 'Do no do the operation')];
436 note that the '?' char is reserved for help
438 Returns: one of the return values from the choices list; if input is
439 not possible (i.e. not running with a tty, we return the last entry
444 choices = [('y', True, 'Perform the operation'),
445 ('n', False, 'Do not perform the operation')]
446 if not choices or not isinstance(choices, list):
447 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
448 for entry in choices:
449 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
450 raise errors.ProgrammerError("Invalid choiches element to AskUser")
452 answer = choices[-1][1]
454 for line in text.splitlines():
455 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
456 text = "\n".join(new_text)
458 f = file("/dev/tty", "a+")
462 chars = [entry[0] for entry in choices]
463 chars[-1] = "[%s]" % chars[-1]
465 maps = dict([(entry[0], entry[1]) for entry in choices])
469 f.write("/".join(chars))
471 line = f.readline(2).strip().lower()
476 for entry in choices:
477 f.write(" %s - %s\n" % (entry[0], entry[2]))
485 class JobSubmittedException(Exception):
486 """Job was submitted, client should exit.
488 This exception has one argument, the ID of the job that was
489 submitted. The handler should print this ID.
491 This is not an error, just a structured way to exit from clients.
496 def SendJob(ops, cl=None):
497 """Function to submit an opcode without waiting for the results.
500 @param ops: list of opcodes
501 @type cl: luxi.Client
502 @param cl: the luxi client to use for communicating with the master;
503 if None, a new client will be created
509 job_id = cl.SubmitJob(ops)
514 def PollJob(job_id, cl=None, feedback_fn=None):
515 """Function to poll for the result of a job.
517 @type job_id: job identified
518 @param job_id: the job to poll for results
519 @type cl: luxi.Client
520 @param cl: the luxi client to use for communicating with the master;
521 if None, a new client will be created
528 prev_logmsg_serial = None
531 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
534 # job not found, go away!
535 raise errors.JobLost("Job with id %s lost" % job_id)
537 # Split result, a tuple of (field values, log entries)
538 (job_info, log_entries) = result
539 (status, ) = job_info
542 for log_entry in log_entries:
543 (serial, timestamp, _, message) = log_entry
544 if callable(feedback_fn):
545 feedback_fn(log_entry[1:])
547 print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), message)
548 prev_logmsg_serial = max(prev_logmsg_serial, serial)
550 # TODO: Handle canceled and archived jobs
551 elif status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
554 prev_job_info = job_info
556 jobs = cl.QueryJobs([job_id], ["status", "opresult"])
558 raise errors.JobLost("Job with id %s lost" % job_id)
560 status, result = jobs[0]
561 if status == constants.JOB_STATUS_SUCCESS:
564 raise errors.OpExecError(result)
567 def SubmitOpCode(op, cl=None, feedback_fn=None):
568 """Legacy function to submit an opcode.
570 This is just a simple wrapper over the construction of the processor
571 instance. It should be extended to better handle feedback and
572 interaction functions.
578 job_id = SendJob([op], cl)
580 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
585 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
586 """Wrapper around SubmitOpCode or SendJob.
588 This function will decide, based on the 'opts' parameter, whether to
589 submit and wait for the result of the opcode (and return it), or
590 whether to just send the job and print its identifier. It is used in
591 order to simplify the implementation of the '--submit' option.
594 if opts and opts.submit_only:
595 job_id = SendJob([op], cl=cl)
596 raise JobSubmittedException(job_id)
598 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
602 # TODO: Cache object?
604 client = luxi.Client()
605 except luxi.NoMasterError:
606 master, myself = ssconf.GetMasterAndMyself()
608 raise errors.OpPrereqError("This is not the master node, please connect"
609 " to node '%s' and rerun the command" %
616 def FormatError(err):
617 """Return a formatted error message for a given error.
619 This function takes an exception instance and returns a tuple
620 consisting of two values: first, the recommended exit code, and
621 second, a string describing the error message (not
628 if isinstance(err, errors.ConfigurationError):
629 txt = "Corrupt configuration file: %s" % msg
631 obuf.write(txt + "\n")
632 obuf.write("Aborting.")
634 elif isinstance(err, errors.HooksAbort):
635 obuf.write("Failure: hooks execution failed:\n")
636 for node, script, out in err.args[0]:
638 obuf.write(" node: %s, script: %s, output: %s\n" %
641 obuf.write(" node: %s, script: %s (no output)\n" %
643 elif isinstance(err, errors.HooksFailure):
644 obuf.write("Failure: hooks general failure: %s" % msg)
645 elif isinstance(err, errors.ResolverError):
646 this_host = utils.HostInfo.SysName()
647 if err.args[0] == this_host:
648 msg = "Failure: can't resolve my own hostname ('%s')"
650 msg = "Failure: can't resolve hostname '%s'"
651 obuf.write(msg % err.args[0])
652 elif isinstance(err, errors.OpPrereqError):
653 obuf.write("Failure: prerequisites not met for this"
654 " operation:\n%s" % msg)
655 elif isinstance(err, errors.OpExecError):
656 obuf.write("Failure: command execution error:\n%s" % msg)
657 elif isinstance(err, errors.TagError):
658 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
659 elif isinstance(err, errors.JobQueueDrainError):
660 obuf.write("Failure: the job queue is marked for drain and doesn't"
661 " accept new requests\n")
662 elif isinstance(err, errors.GenericError):
663 obuf.write("Unhandled Ganeti error: %s" % msg)
664 elif isinstance(err, luxi.NoMasterError):
665 obuf.write("Cannot communicate with the master daemon.\nIs it running"
666 " and listening for connections?")
667 elif isinstance(err, luxi.TimeoutError):
668 obuf.write("Timeout while talking to the master daemon. Error:\n"
670 elif isinstance(err, luxi.ProtocolError):
671 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
673 elif isinstance(err, JobSubmittedException):
674 obuf.write("JobID: %s\n" % err.args[0])
677 obuf.write("Unhandled exception: %s" % msg)
678 return retcode, obuf.getvalue().rstrip('\n')
681 def GenericMain(commands, override=None, aliases=None):
682 """Generic main function for all the gnt-* commands.
685 - commands: a dictionary with a special structure, see the design doc
686 for command line handling.
687 - override: if not None, we expect a dictionary with keys that will
688 override command line options; this can be used to pass
689 options from the scripts to generic functions
690 - aliases: dictionary with command aliases {'alias': 'target, ...}
693 # save the program name and the entire command line for later logging
695 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
696 if len(sys.argv) >= 2:
697 binary += " " + sys.argv[1]
698 old_cmdline = " ".join(sys.argv[2:])
702 binary = "<unknown program>"
708 func, options, args = _ParseArgs(sys.argv, commands, aliases)
709 if func is None: # parse error
712 if override is not None:
713 for key, val in override.iteritems():
714 setattr(options, key, val)
716 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
717 stderr_logging=True, program=binary)
719 utils.debug = options.debug
722 logging.info("run with arguments '%s'", old_cmdline)
724 logging.info("run with no arguments")
727 result = func(options, args)
728 except (errors.GenericError, luxi.ProtocolError,
729 JobSubmittedException), err:
730 result, err_msg = FormatError(err)
731 logging.exception("Error durring command processing")
737 def GenerateTable(headers, fields, separator, data,
738 numfields=None, unitfields=None):
739 """Prints a table with headers and different fields.
742 headers: Dict of header titles or None if no headers should be shown
743 fields: List of fields to show
744 separator: String used to separate fields or None for spaces
745 data: Data to be printed
746 numfields: List of fields to be aligned to right
747 unitfields: List of fields to be formatted as units
750 if numfields is None:
752 if unitfields is None:
757 if headers and field not in headers:
758 raise errors.ProgrammerError("Missing header description for field '%s'"
760 if separator is not None:
761 format_fields.append("%s")
762 elif field in numfields:
763 format_fields.append("%*s")
765 format_fields.append("%-*s")
767 if separator is None:
768 mlens = [0 for name in fields]
769 format = ' '.join(format_fields)
771 format = separator.replace("%", "%%").join(format_fields)
774 for idx, val in enumerate(row):
775 if fields[idx] in unitfields:
781 val = row[idx] = utils.FormatUnit(val)
782 val = row[idx] = str(val)
783 if separator is None:
784 mlens[idx] = max(mlens[idx], len(val))
789 for idx, name in enumerate(fields):
791 if separator is None:
792 mlens[idx] = max(mlens[idx], len(hdr))
793 args.append(mlens[idx])
795 result.append(format % tuple(args))
799 for idx in xrange(len(fields)):
800 if separator is None:
801 args.append(mlens[idx])
802 args.append(line[idx])
803 result.append(format % tuple(args))
808 def FormatTimestamp(ts):
809 """Formats a given timestamp.
812 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
815 @returns: a string with the formatted timestamp
818 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
821 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
824 def ParseTimespec(value):
825 """Parse a time specification.
827 The following suffixed will be recognized:
835 Without any suffix, the value will be taken to be in seconds.
840 raise errors.OpPrereqError("Empty time specification passed")
848 if value[-1] not in suffix_map:
852 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
854 multiplier = suffix_map[value[-1]]
856 if not value: # no data left after stripping the suffix
857 raise errors.OpPrereqError("Invalid time specification (only"
860 value = int(value) * multiplier
862 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
866 def _ToStream(stream, txt, *args):
867 """Write a message to a stream, bypassing the logging system
869 @type stream: file object
870 @param stream: the file to which we should write
872 @param txt: the message
877 stream.write(txt % args)
884 def ToStdout(txt, *args):
885 """Write a message to stdout only, bypassing the logging system
887 This is just a wrapper over _ToStream.
890 @param txt: the message
893 _ToStream(sys.stdout, txt, *args)
896 def ToStderr(txt, *args):
897 """Write a message to stderr only, bypassing the logging system
899 This is just a wrapper over _ToStream.
902 @param txt: the message
905 _ToStream(sys.stderr, txt, *args)