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
26 from cStringIO import StringIO
28 from ganeti import constants
29 from ganeti import cli
30 from ganeti import utils
31 from ganeti import build
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_dbglog() {")
87 sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
94 sw.Write("echo \"$@\"")
98 sw.Write("} >> $GANETI_COMPL_LOG")
106 sw.Write("_ganeti_nodes() {")
109 node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list")
110 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
115 sw.Write("_ganeti_instances() {")
118 instance_list_path = os.path.join(constants.DATA_DIR,
119 "ssconf_instance_list")
120 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
125 sw.Write("_ganeti_jobs() {")
128 # FIXME: this is really going into the internals of the job queue
129 sw.Write(("local jlist=$( shopt -s nullglob &&"
130 " cd %s 2>/dev/null && echo job-* || : )"),
131 utils.ShellQuote(constants.QUEUE_DIR))
132 sw.Write('echo "${jlist//job-/}"')
137 for (fnname, paths) in [
138 ("os", constants.OS_SEARCH_PATH),
139 ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
141 sw.Write("_ganeti_%s() {", fnname)
144 # FIXME: Make querying the master for all OSes cheap
146 sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
147 utils.ShellQuote(path))
152 # Params: <offset> <options with values> <options without values>
153 # Result variable: $first_arg_idx
154 sw.Write("_ganeti_find_first_arg() {")
157 sw.Write("local w i")
159 sw.Write("first_arg_idx=")
160 sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
163 sw.Write("w=${COMP_WORDS[$i]}")
166 sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
169 sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
171 # Ah, we found the first argument
172 sw.Write("else first_arg_idx=$i; break;")
181 # Params: <list of options separated by space>
182 # Input variable: $first_arg_idx
183 # Result variables: $arg_idx, $choices
184 sw.Write("_ganeti_list_options() {")
187 sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
190 sw.Write("arg_idx=0")
191 # Show options only if the current word starts with a dash
192 sw.Write("""if [[ "$cur" == -* ]]; then""")
195 sw.Write("choices=$1")
204 # Calculate position of current argument
205 sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
211 # Params: <long options with equal sign> <all options>
212 # Result variable: $optcur
213 sw.Write("_ganeti_checkopt() {")
216 sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
219 sw.Write("optcur=\"${cur#--*=}\"")
223 sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
226 sw.Write("optcur=\"$cur\"")
232 sw.Write("_ganeti_dbglog optcur=\"'$optcur'\"")
239 # Params: <compgen options>
240 # Result variable: $COMPREPLY
241 sw.Write("_ganeti_compgen() {")
244 sw.Write("""COMPREPLY=( $(compgen "$@") )""")
245 sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
251 def WriteCompReply(sw, args, cur="\"$cur\""):
252 sw.Write("_ganeti_compgen %s -- %s", args, cur)
256 class CompletionWriter:
257 """Command completion writer class.
260 def __init__(self, arg_offset, opts, args):
261 self.arg_offset = arg_offset
266 opt.all_names = sorted(opt._short_opts + opt._long_opts)
268 def _FindFirstArgument(self, sw):
272 for opt in self.opts:
273 if opt.takes_value():
275 for i in opt.all_names:
276 if i.startswith("--"):
277 ignore.append("%s=*" % utils.ShellQuote(i))
278 skip_one.append(utils.ShellQuote(i))
280 ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
282 ignore = sorted(utils.UniqueSequence(ignore))
283 skip_one = sorted(utils.UniqueSequence(skip_one))
285 if ignore or skip_one:
286 # Try to locate first argument
287 sw.Write("_ganeti_find_first_arg %s %s %s",
289 utils.ShellQuote("|".join(skip_one)),
290 utils.ShellQuote("|".join(ignore)))
292 # When there are no options the first argument is always at position
294 sw.Write("first_arg_idx=%s", self.arg_offset + 1)
296 def _CompleteOptionValues(self, sw):
298 # "values" -> [optname1, optname2, ...]
301 for opt in self.opts:
302 if not opt.takes_value():
305 # Only static choices implemented so far (e.g. no node list)
306 suggest = getattr(opt, "completion_suggest", None)
309 suggest = opt.choices
311 if (isinstance(suggest, (int, long)) and
312 suggest in cli.OPT_COMPL_ALL):
315 key = " ".join(sorted(suggest))
319 values.setdefault(key, []).extend(opt.all_names)
321 # Don't write any code if there are no option values
329 for (suggest, allnames) in values.iteritems():
330 longnames = [i for i in allnames if i.startswith("--")]
337 sw.Write("%s _ganeti_checkopt %s %s; then", condcmd,
338 utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
339 utils.ShellQuote("|".join(allnames)))
342 if suggest == cli.OPT_COMPL_MANY_NODES:
343 # TODO: Implement comma-separated values
344 WriteCompReply(sw, "-W ''", cur=cur)
345 elif suggest == cli.OPT_COMPL_ONE_NODE:
346 WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
347 elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
348 WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
349 elif suggest == cli.OPT_COMPL_ONE_OS:
350 WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
351 elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
352 WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
353 elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
354 sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
356 sw.Write("if [[ \"$optcur\" == *:* ]]; then")
359 sw.Write("node1=\"${optcur%%:*}\"")
361 sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
364 sw.Write("pfx=\"$node1:\"")
372 sw.Write("_ganeti_dbglog pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
373 " node1=\"'$node1'\"")
375 sw.Write("for i in $(_ganeti_nodes); do")
378 sw.Write("if [[ -z \"$node1\" ]]; then")
381 sw.Write("tmp=\"$tmp $i $i:\"")
384 sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
387 sw.Write("tmp=\"$tmp $i\"")
395 WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
397 WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
408 def _CompleteArguments(self, sw):
409 if not (self.opts or self.args):
412 all_option_names = []
413 for opt in self.opts:
414 all_option_names.extend(opt.all_names)
415 all_option_names.sort()
417 # List options if no argument has been specified yet
418 sw.Write("_ganeti_list_options %s",
419 utils.ShellQuote(" ".join(all_option_names)))
422 last_idx = len(self.args) - 1
424 varlen_arg_idx = None
427 # Write some debug comments
428 for idx, arg in enumerate(self.args):
429 sw.Write("# %s: %r", idx, arg)
431 sw.Write("compgenargs=")
433 for idx, arg in enumerate(self.args):
434 assert arg.min is not None and arg.min >= 0
435 assert not (idx < last_idx and arg.max is None)
437 if arg.min != arg.max or arg.max is None:
438 if varlen_arg_idx is not None:
439 raise Exception("Only one argument can have a variable length")
444 if isinstance(arg, cli.ArgUnknown):
446 elif isinstance(arg, cli.ArgSuggest):
447 choices = utils.ShellQuote(" ".join(arg.choices))
448 elif isinstance(arg, cli.ArgInstance):
449 choices = "$(_ganeti_instances)"
450 elif isinstance(arg, cli.ArgNode):
451 choices = "$(_ganeti_nodes)"
452 elif isinstance(arg, cli.ArgJobId):
453 choices = "$(_ganeti_jobs)"
454 elif isinstance(arg, cli.ArgFile):
456 compgenargs.append("-f")
457 elif isinstance(arg, cli.ArgCommand):
459 compgenargs.append("-c")
460 elif isinstance(arg, cli.ArgHost):
462 compgenargs.append("-A hostname")
464 raise Exception("Unknown argument type %r" % arg)
466 if arg.min == 1 and arg.max == 1:
467 cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
468 elif arg.min <= arg.max:
469 cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
470 (last_arg_end, last_arg_end + arg.max))
471 elif arg.max is None:
472 cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
474 raise Exception("Unable to generate argument position condition")
476 last_arg_end += arg.min
478 if choices or compgenargs:
484 sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
488 sw.Write("""choices="$choices "%s""", choices)
490 sw.Write("compgenargs=%s", utils.ShellQuote(" ".join(compgenargs)))
500 WriteCompReply(sw, """-W "$choices" $compgenargs""")
502 # $compgenargs exists only if there are arguments
503 WriteCompReply(sw, '-W "$choices"')
505 def WriteTo(self, sw):
506 self._FindFirstArgument(sw)
507 self._CompleteOptionValues(sw)
508 self._CompleteArguments(sw)
511 def WriteCompletion(sw, scriptname, funcname,
513 opts=None, args=None):
514 """Writes the completion code for one command.
516 @type sw: ShellWriter
517 @param sw: Script writer
518 @type scriptname: string
519 @param scriptname: Name of command line program
520 @type funcname: string
521 @param funcname: Shell function name
523 @param commands: List of all subcommands in this program
526 sw.Write("%s() {", funcname)
530 ' cur="${COMP_WORDS[COMP_CWORD]}"'
531 ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
532 ' i first_arg_idx choices compgenargs arg_idx optcur')
534 sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
535 sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
536 " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
538 sw.Write("COMPREPLY=()")
540 if opts is not None and args is not None:
542 CompletionWriter(0, opts, args).WriteTo(sw)
545 sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
548 # Complete the command name
551 utils.ShellQuote(" ".join(sorted(commands.keys())))))
556 # We're doing options and arguments to commands
557 sw.Write("""case "${COMP_WORDS[1]}" in""")
558 for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
559 if not (argdef or optdef):
562 # TODO: Group by arguments and options
563 sw.Write("%s)", utils.ShellQuote(cmd))
566 CompletionWriter(1, optdef, argdef).WriteTo(sw)
576 sw.Write("complete -F %s -o filenames %s",
577 utils.ShellQuote(funcname),
578 utils.ShellQuote(scriptname))
581 def GetFunctionName(name):
582 return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
585 def GetCommands(filename, module):
586 """Returns the commands defined in a module.
588 Aliases are also added as commands.
592 commands = getattr(module, "commands")
593 except AttributeError, err:
594 raise Exception("Script %s doesn't have 'commands' attribute" %
597 # Add the implicit "--help" option
598 help_option = cli.cli_option("-h", "--help", default=False,
601 for (_, _, optdef, _, _) in commands.itervalues():
602 if help_option not in optdef:
603 optdef.append(help_option)
604 if cli.DEBUG_OPT not in optdef:
605 optdef.append(cli.DEBUG_OPT)
608 aliases = getattr(module, "aliases", {})
610 commands = commands.copy()
611 for name, target in aliases.iteritems():
612 commands[name] = commands[target]
619 sw = ShellWriter(buf)
624 for scriptname in _autoconf.GNT_SCRIPTS:
625 filename = "scripts/%s" % scriptname
627 WriteCompletion(sw, scriptname,
628 GetFunctionName(scriptname),
629 commands=GetCommands(filename,
630 build.LoadModule(filename)))
633 burnin = build.LoadModule("tools/burnin")
634 WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
635 opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
640 if __name__ == "__main__":