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 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
478 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
479 """Wrapper around SubmitOpCode or SendJob.
481 This function will decide, based on the 'opts' parameter, whether to
482 submit and wait for the result of the opcode (and return it), or
483 whether to just send the job and print its identifier. It is used in
484 order to simplify the implementation of the '--submit' option.
487 if opts and opts.submit_only:
488 job_id = SendJob([op], cl=cl)
489 raise JobSubmittedException(job_id)
491 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
495 # TODO: Cache object?
497 client = luxi.Client()
498 except luxi.NoMasterError:
499 master, myself = ssconf.GetMasterAndMyself()
501 raise errors.OpPrereqError("This is not the master node, please connect"
502 " to node '%s' and rerun the command" %
509 def FormatError(err):
510 """Return a formatted error message for a given error.
512 This function takes an exception instance and returns a tuple
513 consisting of two values: first, the recommended exit code, and
514 second, a string describing the error message (not
521 if isinstance(err, errors.ConfigurationError):
522 txt = "Corrupt configuration file: %s" % msg
524 obuf.write(txt + "\n")
525 obuf.write("Aborting.")
527 elif isinstance(err, errors.HooksAbort):
528 obuf.write("Failure: hooks execution failed:\n")
529 for node, script, out in err.args[0]:
531 obuf.write(" node: %s, script: %s, output: %s\n" %
534 obuf.write(" node: %s, script: %s (no output)\n" %
536 elif isinstance(err, errors.HooksFailure):
537 obuf.write("Failure: hooks general failure: %s" % msg)
538 elif isinstance(err, errors.ResolverError):
539 this_host = utils.HostInfo.SysName()
540 if err.args[0] == this_host:
541 msg = "Failure: can't resolve my own hostname ('%s')"
543 msg = "Failure: can't resolve hostname '%s'"
544 obuf.write(msg % err.args[0])
545 elif isinstance(err, errors.OpPrereqError):
546 obuf.write("Failure: prerequisites not met for this"
547 " operation:\n%s" % msg)
548 elif isinstance(err, errors.OpExecError):
549 obuf.write("Failure: command execution error:\n%s" % msg)
550 elif isinstance(err, errors.TagError):
551 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
552 elif isinstance(err, errors.GenericError):
553 obuf.write("Unhandled Ganeti error: %s" % msg)
554 elif isinstance(err, luxi.NoMasterError):
555 obuf.write("Cannot communicate with the master daemon.\nIs it running"
556 " and listening for connections?")
557 elif isinstance(err, luxi.TimeoutError):
558 obuf.write("Timeout while talking to the master daemon. Error:\n"
560 elif isinstance(err, luxi.ProtocolError):
561 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
563 elif isinstance(err, JobSubmittedException):
564 obuf.write("JobID: %s\n" % err.args[0])
567 obuf.write("Unhandled exception: %s" % msg)
568 return retcode, obuf.getvalue().rstrip('\n')
571 def GenericMain(commands, override=None, aliases=None):
572 """Generic main function for all the gnt-* commands.
575 - commands: a dictionary with a special structure, see the design doc
576 for command line handling.
577 - override: if not None, we expect a dictionary with keys that will
578 override command line options; this can be used to pass
579 options from the scripts to generic functions
580 - aliases: dictionary with command aliases {'alias': 'target, ...}
583 # save the program name and the entire command line for later logging
585 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
586 if len(sys.argv) >= 2:
587 binary += " " + sys.argv[1]
588 old_cmdline = " ".join(sys.argv[2:])
592 binary = "<unknown program>"
598 func, options, args = _ParseArgs(sys.argv, commands, aliases)
599 if func is None: # parse error
602 if override is not None:
603 for key, val in override.iteritems():
604 setattr(options, key, val)
606 logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
607 stderr_logging=True, program=binary)
609 utils.debug = options.debug
612 logger.Info("run with arguments '%s'" % old_cmdline)
614 logger.Info("run with no arguments")
617 result = func(options, args)
618 except (errors.GenericError, luxi.ProtocolError), err:
619 result, err_msg = FormatError(err)
620 logger.ToStderr(err_msg)
625 def GenerateTable(headers, fields, separator, data,
626 numfields=None, unitfields=None):
627 """Prints a table with headers and different fields.
630 headers: Dict of header titles or None if no headers should be shown
631 fields: List of fields to show
632 separator: String used to separate fields or None for spaces
633 data: Data to be printed
634 numfields: List of fields to be aligned to right
635 unitfields: List of fields to be formatted as units
638 if numfields is None:
640 if unitfields is None:
645 if headers and field not in headers:
646 raise errors.ProgrammerError("Missing header description for field '%s'"
648 if separator is not None:
649 format_fields.append("%s")
650 elif field in numfields:
651 format_fields.append("%*s")
653 format_fields.append("%-*s")
655 if separator is None:
656 mlens = [0 for name in fields]
657 format = ' '.join(format_fields)
659 format = separator.replace("%", "%%").join(format_fields)
662 for idx, val in enumerate(row):
663 if fields[idx] in unitfields:
669 val = row[idx] = utils.FormatUnit(val)
670 val = row[idx] = str(val)
671 if separator is None:
672 mlens[idx] = max(mlens[idx], len(val))
677 for idx, name in enumerate(fields):
679 if separator is None:
680 mlens[idx] = max(mlens[idx], len(hdr))
681 args.append(mlens[idx])
683 result.append(format % tuple(args))
687 for idx in xrange(len(fields)):
688 if separator is None:
689 args.append(mlens[idx])
690 args.append(line[idx])
691 result.append(format % tuple(args))
696 def FormatTimestamp(ts):
697 """Formats a given timestamp.
700 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
703 @returns: a string with the formatted timestamp
706 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
709 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
712 def ParseTimespec(value):
713 """Parse a time specification.
715 The following suffixed will be recognized:
723 Without any suffix, the value will be taken to be in seconds.
728 raise errors.OpPrereqError("Empty time specification passed")
736 if value[-1] not in suffix_map:
740 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
742 multiplier = suffix_map[value[-1]]
744 if not value: # no data left after stripping the suffix
745 raise errors.OpPrereqError("Invalid time specification (only"
748 value = int(value) * multiplier
750 raise errors.OpPrereqError("Invalid time specification '%s'" % value)