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",
48 "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
49 "FormatError", "SplitNodeOption"
53 def _ExtractTagsObject(opts, args):
54 """Extract the tag type object.
56 Note that this function will modify its args parameter.
59 if not hasattr(opts, "tag_type"):
60 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
62 if kind == constants.TAG_CLUSTER:
64 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
66 raise errors.OpPrereqError("no arguments passed to the command")
70 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
74 def _ExtendTags(opts, args):
75 """Extend the args if a source file has been given.
77 This function will extend the tags with the contents of the file
78 passed in the 'tags_source' attribute of the opts parameter. A file
79 named '-' will be replaced by stdin.
82 fname = opts.tags_source
88 new_fh = open(fname, "r")
91 # we don't use the nice 'new_data = [line.strip() for line in fh]'
92 # because of python bug 1633941
94 line = new_fh.readline()
97 new_data.append(line.strip())
100 args.extend(new_data)
103 def ListTags(opts, args):
104 """List the tags on a given object.
106 This is a generic implementation that knows how to deal with all
107 three cases of tag objects (cluster, node, instance). The opts
108 argument is expected to contain a tag_type field denoting what
109 object type we work on.
112 kind, name = _ExtractTagsObject(opts, args)
113 op = opcodes.OpGetTags(kind=kind, name=name)
114 result = SubmitOpCode(op)
115 result = list(result)
121 def AddTags(opts, args):
122 """Add tags on a given object.
124 This is a generic implementation that knows how to deal with all
125 three cases of tag objects (cluster, node, instance). The opts
126 argument is expected to contain a tag_type field denoting what
127 object type we work on.
130 kind, name = _ExtractTagsObject(opts, args)
131 _ExtendTags(opts, args)
133 raise errors.OpPrereqError("No tags to be added")
134 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
138 def RemoveTags(opts, args):
139 """Remove tags from a given object.
141 This is a generic implementation that knows how to deal with all
142 three cases of tag objects (cluster, node, instance). The opts
143 argument is expected to contain a tag_type field denoting what
144 object type we work on.
147 kind, name = _ExtractTagsObject(opts, args)
148 _ExtendTags(opts, args)
150 raise errors.OpPrereqError("No tags to be removed")
151 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
155 DEBUG_OPT = make_option("-d", "--debug", default=False,
157 help="Turn debugging on")
159 NOHDR_OPT = make_option("--no-headers", default=False,
160 action="store_true", dest="no_headers",
161 help="Don't display column headers")
163 SEP_OPT = make_option("--separator", default=None,
164 action="store", dest="separator",
165 help="Separator between output fields"
166 " (defaults to one space)")
168 USEUNITS_OPT = make_option("--human-readable", default=False,
169 action="store_true", dest="human_readable",
170 help="Print sizes in human readable format")
172 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
173 type="string", help="Comma separated list of"
177 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
178 default=False, help="Force the operation")
180 TAG_SRC_OPT = make_option("--from", dest="tags_source",
181 default=None, help="File with tag names")
185 """Macro-like function denoting a fixed number of arguments"""
189 def ARGS_ATLEAST(val):
190 """Macro-like function denoting a minimum number of arguments"""
195 ARGS_ONE = ARGS_FIXED(1)
196 ARGS_ANY = ARGS_ATLEAST(0)
199 def check_unit(option, opt, value):
200 """OptParsers custom converter for units.
204 return utils.ParseUnit(value)
205 except errors.UnitParseError, err:
206 raise OptionValueError("option %s: %s" % (opt, err))
209 class CliOption(Option):
210 """Custom option class for optparse.
213 TYPES = Option.TYPES + ("unit",)
214 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
215 TYPE_CHECKER["unit"] = check_unit
218 # optparse.py sets make_option, so we do it for our own option class, too
219 cli_option = CliOption
222 def _ParseArgs(argv, commands, aliases):
223 """Parses the command line and return the function which must be
224 executed together with its arguments
227 argv: the command line
229 commands: dictionary with special contents, see the design doc for
231 aliases: dictionary with command aliases {'alias': 'target, ...}
237 binary = argv[0].split("/")[-1]
239 if len(argv) > 1 and argv[1] == "--version":
240 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
241 # Quit right away. That way we don't have to care about this special
242 # argument. optparse.py does it the same.
245 if len(argv) < 2 or not (argv[1] in commands or
247 # let's do a nice thing
248 sortedcmds = commands.keys()
250 print ("Usage: %(bin)s {command} [options...] [argument...]"
251 "\n%(bin)s <command> --help to see details, or"
252 " man %(bin)s\n" % {"bin": binary})
253 # compute the max line length for cmd + usage
254 mlen = max([len(" %s" % cmd) for cmd in commands])
255 mlen = min(60, mlen) # should not get here...
256 # and format a nice command list
258 for cmd in sortedcmds:
259 cmdstr = " %s" % (cmd,)
260 help_text = commands[cmd][4]
261 help_lines = textwrap.wrap(help_text, 79-3-mlen)
262 print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
263 for line in help_lines:
264 print "%-*s %s" % (mlen, "", line)
266 return None, None, None
268 # get command, unalias it, and look it up in commands
272 raise errors.ProgrammerError("Alias '%s' overrides an existing"
275 if aliases[cmd] not in commands:
276 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
277 " command '%s'" % (cmd, aliases[cmd]))
281 func, nargs, parser_opts, usage, description = commands[cmd]
282 parser = OptionParser(option_list=parser_opts,
283 description=description,
284 formatter=TitledHelpFormatter(),
285 usage="%%prog %s %s" % (cmd, usage))
286 parser.disable_interspersed_args()
287 options, args = parser.parse_args()
290 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
291 return None, None, None
292 elif nargs < 0 and len(args) != -nargs:
293 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
295 return None, None, None
296 elif nargs >= 0 and len(args) < nargs:
297 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
299 return None, None, None
301 return func, options, args
304 def SplitNodeOption(value):
305 """Splits the value of a --node option.
308 if value and ':' in value:
309 return value.split(':', 1)
314 def AskUser(text, choices=None):
315 """Ask the user a question.
318 text - the question to ask.
320 choices - list with elements tuples (input_char, return_value,
321 description); if not given, it will default to: [('y', True,
322 'Perform the operation'), ('n', False, 'Do no do the operation')];
323 note that the '?' char is reserved for help
325 Returns: one of the return values from the choices list; if input is
326 not possible (i.e. not running with a tty, we return the last entry
331 choices = [('y', True, 'Perform the operation'),
332 ('n', False, 'Do not perform the operation')]
333 if not choices or not isinstance(choices, list):
334 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
335 for entry in choices:
336 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
337 raise errors.ProgrammerError("Invalid choiches element to AskUser")
339 answer = choices[-1][1]
341 for line in text.splitlines():
342 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
343 text = "\n".join(new_text)
345 f = file("/dev/tty", "a+")
349 chars = [entry[0] for entry in choices]
350 chars[-1] = "[%s]" % chars[-1]
352 maps = dict([(entry[0], entry[1]) for entry in choices])
356 f.write("/".join(chars))
358 line = f.readline(2).strip().lower()
363 for entry in choices:
364 f.write(" %s - %s\n" % (entry[0], entry[2]))
372 def SendJob(ops, cl=None):
373 """Function to submit an opcode without waiting for the results.
376 @param ops: list of opcodes
377 @type cl: luxi.Client
378 @param cl: the luxi client to use for communicating with the master;
379 if None, a new client will be created
385 job_id = cl.SubmitJob(ops)
390 def PollJob(job_id, cl=None):
391 """Function to poll for the result of a job.
393 @type job_id: job identified
394 @param job_id: the job to poll for results
395 @type cl: luxi.Client
396 @param cl: the luxi client to use for communicating with the master;
397 if None, a new client will be created
405 jobs = cl.QueryJobs([job_id], ["status", "ticker"])
407 # job not found, go away!
408 raise errors.JobLost("Job with id %s lost" % job_id)
410 # TODO: Handle canceled and archived jobs
412 if status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
415 if msg is not None and msg != lastmsg:
416 if callable(feedback_fn):
419 print "%s %s" % (time.ctime(msg[0]), msg[2])
423 jobs = cl.QueryJobs([job_id], ["status", "opresult"])
425 raise errors.JobLost("Job with id %s lost" % job_id)
427 status, result = jobs[0]
428 if status == constants.JOB_STATUS_SUCCESS:
431 raise errors.OpExecError(result)
434 def SubmitOpCode(op, cl=None, feedback_fn=None):
435 """Legacy function to submit an opcode.
437 This is just a simple wrapper over the construction of the processor
438 instance. It should be extended to better handle feedback and
439 interaction functions.
445 job_id = SendJob([op], cl)
447 return PollJob(job_id, cl)
451 # TODO: Cache object?
453 client = luxi.Client()
454 except luxi.NoMasterError:
455 master, myself = ssconf.GetMasterAndMyself()
457 raise errors.OpPrereqError("This is not the master node, please connect"
458 " to node '%s' and rerun the command" %
465 def FormatError(err):
466 """Return a formatted error message for a given error.
468 This function takes an exception instance and returns a tuple
469 consisting of two values: first, the recommended exit code, and
470 second, a string describing the error message (not
477 if isinstance(err, errors.ConfigurationError):
478 txt = "Corrupt configuration file: %s" % msg
480 obuf.write(txt + "\n")
481 obuf.write("Aborting.")
483 elif isinstance(err, errors.HooksAbort):
484 obuf.write("Failure: hooks execution failed:\n")
485 for node, script, out in err.args[0]:
487 obuf.write(" node: %s, script: %s, output: %s\n" %
490 obuf.write(" node: %s, script: %s (no output)\n" %
492 elif isinstance(err, errors.HooksFailure):
493 obuf.write("Failure: hooks general failure: %s" % msg)
494 elif isinstance(err, errors.ResolverError):
495 this_host = utils.HostInfo.SysName()
496 if err.args[0] == this_host:
497 msg = "Failure: can't resolve my own hostname ('%s')"
499 msg = "Failure: can't resolve hostname '%s'"
500 obuf.write(msg % err.args[0])
501 elif isinstance(err, errors.OpPrereqError):
502 obuf.write("Failure: prerequisites not met for this"
503 " operation:\n%s" % msg)
504 elif isinstance(err, errors.OpExecError):
505 obuf.write("Failure: command execution error:\n%s" % msg)
506 elif isinstance(err, errors.TagError):
507 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
508 elif isinstance(err, errors.GenericError):
509 obuf.write("Unhandled Ganeti error: %s" % msg)
510 elif isinstance(err, luxi.NoMasterError):
511 obuf.write("Cannot communicate with the master daemon.\nIs it running"
512 " and listening on '%s'?" % err.args[0])
513 elif isinstance(err, luxi.TimeoutError):
514 obuf.write("Timeout while talking to the master daemon. Error:\n"
516 elif isinstance(err, luxi.ProtocolError):
517 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
520 obuf.write("Unhandled exception: %s" % msg)
521 return retcode, obuf.getvalue().rstrip('\n')
524 def GenericMain(commands, override=None, aliases=None):
525 """Generic main function for all the gnt-* commands.
528 - commands: a dictionary with a special structure, see the design doc
529 for command line handling.
530 - override: if not None, we expect a dictionary with keys that will
531 override command line options; this can be used to pass
532 options from the scripts to generic functions
533 - aliases: dictionary with command aliases {'alias': 'target, ...}
536 # save the program name and the entire command line for later logging
538 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
539 if len(sys.argv) >= 2:
540 binary += " " + sys.argv[1]
541 old_cmdline = " ".join(sys.argv[2:])
545 binary = "<unknown program>"
551 func, options, args = _ParseArgs(sys.argv, commands, aliases)
552 if func is None: # parse error
555 if override is not None:
556 for key, val in override.iteritems():
557 setattr(options, key, val)
559 logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
560 stderr_logging=True, program=binary)
562 utils.debug = options.debug
565 logger.Info("run with arguments '%s'" % old_cmdline)
567 logger.Info("run with no arguments")
570 result = func(options, args)
571 except (errors.GenericError, luxi.ProtocolError), err:
572 result, err_msg = FormatError(err)
573 logger.ToStderr(err_msg)
578 def GenerateTable(headers, fields, separator, data,
579 numfields=None, unitfields=None):
580 """Prints a table with headers and different fields.
583 headers: Dict of header titles or None if no headers should be shown
584 fields: List of fields to show
585 separator: String used to separate fields or None for spaces
586 data: Data to be printed
587 numfields: List of fields to be aligned to right
588 unitfields: List of fields to be formatted as units
591 if numfields is None:
593 if unitfields is None:
598 if headers and field not in headers:
599 raise errors.ProgrammerError("Missing header description for field '%s'"
601 if separator is not None:
602 format_fields.append("%s")
603 elif field in numfields:
604 format_fields.append("%*s")
606 format_fields.append("%-*s")
608 if separator is None:
609 mlens = [0 for name in fields]
610 format = ' '.join(format_fields)
612 format = separator.replace("%", "%%").join(format_fields)
615 for idx, val in enumerate(row):
616 if fields[idx] in unitfields:
622 val = row[idx] = utils.FormatUnit(val)
623 val = row[idx] = str(val)
624 if separator is None:
625 mlens[idx] = max(mlens[idx], len(val))
630 for idx, name in enumerate(fields):
632 if separator is None:
633 mlens[idx] = max(mlens[idx], len(hdr))
634 args.append(mlens[idx])
636 result.append(format % tuple(args))
640 for idx in xrange(len(fields)):
641 if separator is None:
642 args.append(mlens[idx])
643 args.append(line[idx])
644 result.append(format % tuple(args))