4 # Copyright (C) 2009 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
27 from cStringIO import StringIO
29 from ganeti import constants
30 from ganeti import cli
31 from ganeti import utils
33 # _autoconf shouldn't be imported from anywhere except constants.py, but we're
34 # making an exception here because this script is only used at build time.
35 from ganeti import _autoconf
39 """Helper class to write scripts with indentation.
44 def __init__(self, fh):
49 """Increase indentation level by 1.
55 """Decrease indentation level by 1.
58 assert self._indent > 0
61 def Write(self, txt, *args):
62 """Write line to output file.
65 self._fh.write(self._indent * self.INDENT_STR)
68 self._fh.write(txt % args)
75 def WritePreamble(sw):
76 """Writes the script preamble.
78 Helper functions should be written here.
81 sw.Write("# This script is automatically generated at build time.")
82 sw.Write("# Do not modify manually.")
84 sw.Write("_ganeti_nodes() {")
87 node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list")
88 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
93 sw.Write("_ganeti_instances() {")
96 instance_list_path = os.path.join(constants.DATA_DIR,
97 "ssconf_instance_list")
98 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
103 sw.Write("_ganeti_jobs() {")
106 # FIXME: this is really going into the internals of the job queue
107 sw.Write(("local jlist=$( shopt -s nullglob &&"
108 " cd %s 2>/dev/null && echo job-* || : )"),
109 utils.ShellQuote(constants.QUEUE_DIR))
110 sw.Write('echo "${jlist//job-/}"')
115 sw.Write("_ganeti_os() {")
118 # FIXME: Make querying the master for all OSes cheap
119 for path in constants.OS_SEARCH_PATH:
120 sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
121 utils.ShellQuote(path))
126 # Params: <offset> <options with values> <options without values>
127 # Result variable: $first_arg_idx
128 sw.Write("_ganeti_find_first_arg() {")
131 sw.Write("local w i")
133 sw.Write("first_arg_idx=")
134 sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
137 sw.Write("w=${COMP_WORDS[$i]}")
140 sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
143 sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
145 # Ah, we found the first argument
146 sw.Write("else first_arg_idx=$i; break;")
155 # Params: <list of options separated by space>
156 # Input variable: $first_arg_idx
157 # Result variables: $arg_idx, $choices
158 sw.Write("_ganeti_list_options() {")
161 sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
164 sw.Write("arg_idx=0")
165 # Show options only if the current word starts with a dash
166 sw.Write("""if [[ "$cur" == -* ]]; then""")
169 sw.Write("choices=$1")
178 # Calculate position of current argument
179 sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
186 def WriteCompReply(sw, args, cur="\"$cur\""):
187 sw.Write("""COMPREPLY=( $(compgen %s -- %s) )""", args, cur)
191 class CompletionWriter:
192 """Command completion writer class.
195 def __init__(self, arg_offset, opts, args):
196 self.arg_offset = arg_offset
201 opt.all_names = sorted(opt._short_opts + opt._long_opts)
203 def _FindFirstArgument(self, sw):
207 for opt in self.opts:
208 if opt.takes_value():
210 for i in opt.all_names:
211 if i.startswith("--"):
212 ignore.append("%s=*" % utils.ShellQuote(i))
213 skip_one.append(utils.ShellQuote(i))
215 ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
217 ignore = sorted(utils.UniqueSequence(ignore))
218 skip_one = sorted(utils.UniqueSequence(skip_one))
220 if ignore or skip_one:
221 # Try to locate first argument
222 sw.Write("_ganeti_find_first_arg %s %s %s",
224 utils.ShellQuote("|".join(skip_one)),
225 utils.ShellQuote("|".join(ignore)))
227 # When there are no options the first argument is always at position
229 sw.Write("first_arg_idx=%s", self.arg_offset + 1)
231 def _CompleteOptionValues(self, sw):
233 # "values" -> [optname1, optname2, ...]
236 for opt in self.opts:
237 if not opt.takes_value():
240 # Only static choices implemented so far (e.g. no node list)
241 suggest = getattr(opt, "completion_suggest", None)
244 suggest = opt.choices
247 suggest_text = " ".join(sorted(suggest))
251 values.setdefault(suggest_text, []).extend(opt.all_names)
253 # Don't write any code if there are no option values
257 sw.Write("if [[ $COMP_CWORD -gt %s ]]; then", self.arg_offset + 1)
261 sw.Write("""case "$prev" in""")
262 for (choices, names) in values.iteritems():
263 sw.Write("%s)", "|".join([utils.ShellQuote(i) for i in names]))
266 WriteCompReply(sw, "-W %s" % utils.ShellQuote(choices))
278 for (choices, names) in values.iteritems():
279 longnames = [i for i in names if i.startswith("--")]
281 values_longopts[choices] = longnames
284 sw.Write("""case "$cur" in""")
285 for (choices, names) in values_longopts.iteritems():
286 sw.Write("%s)", "|".join([utils.ShellQuote(i) + "=*" for i in names]))
289 # Shell expression to get option value
290 cur="\"${cur#--*=}\""
291 WriteCompReply(sw, "-W %s" % utils.ShellQuote(choices), cur=cur)
297 def _CompleteArguments(self, sw):
298 if not (self.opts or self.args):
301 all_option_names = []
302 for opt in self.opts:
303 all_option_names.extend(opt.all_names)
304 all_option_names.sort()
306 # List options if no argument has been specified yet
307 sw.Write("_ganeti_list_options %s",
308 utils.ShellQuote(" ".join(all_option_names)))
311 last_idx = len(self.args) - 1
313 varlen_arg_idx = None
316 # Write some debug comments
317 for idx, arg in enumerate(self.args):
318 sw.Write("# %s: %r", idx, arg)
320 sw.Write("compgenargs=")
322 for idx, arg in enumerate(self.args):
323 assert arg.min is not None and arg.min >= 0
324 assert not (idx < last_idx and arg.max is None)
326 if arg.min != arg.max or arg.max is None:
327 if varlen_arg_idx is not None:
328 raise Exception("Only one argument can have a variable length")
333 if isinstance(arg, cli.ArgUnknown):
335 elif isinstance(arg, cli.ArgSuggest):
336 choices = utils.ShellQuote(" ".join(arg.choices))
337 elif isinstance(arg, cli.ArgInstance):
338 choices = "$(_ganeti_instances)"
339 elif isinstance(arg, cli.ArgNode):
340 choices = "$(_ganeti_nodes)"
341 elif isinstance(arg, cli.ArgJobId):
342 choices = "$(_ganeti_jobs)"
343 elif isinstance(arg, cli.ArgFile):
345 compgenargs.append("-f")
346 elif isinstance(arg, cli.ArgCommand):
348 compgenargs.append("-c")
349 elif isinstance(arg, cli.ArgHost):
351 compgenargs.append("-A hostname")
353 raise Exception("Unknown argument type %r" % arg)
355 if arg.min == 1 and arg.max == 1:
356 cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
357 elif arg.min <= arg.max:
358 cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
359 (last_arg_end, last_arg_end + arg.max))
360 elif arg.max is None:
361 cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
363 raise Exception("Unable to generate argument position condition")
365 last_arg_end += arg.min
367 if choices or compgenargs:
373 sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
377 sw.Write("""choices="$choices "%s""", choices)
379 sw.Write("compgenargs=%s", utils.ShellQuote(" ".join(compgenargs)))
389 WriteCompReply(sw, """-W "$choices" $compgenargs""")
391 # $compgenargs exists only if there are arguments
392 WriteCompReply(sw, '-W "$choices"')
394 def WriteTo(self, sw):
395 self._FindFirstArgument(sw)
396 self._CompleteOptionValues(sw)
397 self._CompleteArguments(sw)
400 def WriteCompletion(sw, scriptname, funcname,
402 opts=None, args=None):
403 """Writes the completion code for one command.
405 @type sw: ShellWriter
406 @param sw: Script writer
407 @type scriptname: string
408 @param scriptname: Name of command line program
409 @type funcname: string
410 @param funcname: Shell function name
412 @param commands: List of all subcommands in this program
415 sw.Write("%s() {", funcname)
419 ' cur="${COMP_WORDS[COMP_CWORD]}"'
420 ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
421 ' i first_arg_idx choices compgenargs arg_idx')
423 # Useful for debugging:
424 #sw.Write("echo cur=\"$cur\" prev=\"$prev\"")
425 #sw.Write("set | grep ^COMP_")
427 sw.Write("COMPREPLY=()")
429 if opts is not None and args is not None:
431 CompletionWriter(0, opts, args).WriteTo(sw)
434 sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
437 # Complete the command name
440 utils.ShellQuote(" ".join(sorted(commands.keys())))))
445 # We're doing options and arguments to commands
446 sw.Write("""case "${COMP_WORDS[1]}" in""")
447 for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
448 if not (argdef or optdef):
451 # TODO: Group by arguments and options
452 sw.Write("%s)", utils.ShellQuote(cmd))
455 CompletionWriter(1, optdef, argdef).WriteTo(sw)
465 sw.Write("complete -F %s -o filenames %s",
466 utils.ShellQuote(funcname),
467 utils.ShellQuote(scriptname))
470 def GetFunctionName(name):
471 return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
474 def LoadModule(filename):
475 """Loads an external module by filename.
478 (name, ext) = os.path.splitext(filename)
480 fh = open(filename, "U")
482 return imp.load_module(name, fh, filename, (ext, "U", imp.PY_SOURCE))
487 def GetCommands(filename, module):
488 """Returns the commands defined in a module.
490 Aliases are also added as commands.
494 commands = getattr(module, "commands")
495 except AttributeError, err:
496 raise Exception("Script %s doesn't have 'commands' attribute" %
500 aliases = getattr(module, "aliases", {})
502 commands = commands.copy()
503 for name, target in aliases.iteritems():
504 commands[name] = commands[target]
511 sw = ShellWriter(buf)
516 for scriptname in _autoconf.GNT_SCRIPTS:
517 filename = "scripts/%s" % scriptname
519 WriteCompletion(sw, scriptname,
520 GetFunctionName(scriptname),
521 commands=GetCommands(filename, LoadModule(filename)))
524 burnin = LoadModule("tools/burnin")
525 WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
526 opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
531 if __name__ == "__main__":