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
24 from cStringIO import StringIO
26 from ganeti import constants
27 from ganeti import cli
28 from ganeti import utils
29 from ganeti import build
31 # _autoconf shouldn't be imported from anywhere except constants.py, but we're
32 # making an exception here because this script is only used at build time.
33 from ganeti import _autoconf
37 """Helper class to write scripts with indentation.
42 def __init__(self, fh):
47 """Increase indentation level by 1.
53 """Decrease indentation level by 1.
56 assert self._indent > 0
59 def Write(self, txt, *args):
60 """Write line to output file.
63 self._fh.write(self._indent * self.INDENT_STR)
66 self._fh.write(txt % args)
73 def WritePreamble(sw):
74 """Writes the script preamble.
76 Helper functions should be written here.
79 sw.Write("# This script is automatically generated at build time.")
80 sw.Write("# Do not modify manually.")
82 sw.Write("_ganeti_dbglog() {")
85 sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
92 sw.Write("echo \"$@\"")
96 sw.Write("} >> $GANETI_COMPL_LOG")
104 sw.Write("_ganeti_nodes() {")
107 node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list")
108 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
113 sw.Write("_ganeti_instances() {")
116 instance_list_path = os.path.join(constants.DATA_DIR,
117 "ssconf_instance_list")
118 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
123 sw.Write("_ganeti_jobs() {")
126 # FIXME: this is really going into the internals of the job queue
127 sw.Write(("local jlist=$( shopt -s nullglob &&"
128 " cd %s 2>/dev/null && echo job-* || : )"),
129 utils.ShellQuote(constants.QUEUE_DIR))
130 sw.Write('echo "${jlist//job-/}"')
135 for (fnname, paths) in [
136 ("os", constants.OS_SEARCH_PATH),
137 ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
139 sw.Write("_ganeti_%s() {", fnname)
142 # FIXME: Make querying the master for all OSes cheap
144 sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
145 utils.ShellQuote(path))
150 # Params: <offset> <options with values> <options without values>
151 # Result variable: $first_arg_idx
152 sw.Write("_ganeti_find_first_arg() {")
155 sw.Write("local w i")
157 sw.Write("first_arg_idx=")
158 sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
161 sw.Write("w=${COMP_WORDS[$i]}")
164 sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
167 sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
169 # Ah, we found the first argument
170 sw.Write("else first_arg_idx=$i; break;")
179 # Params: <list of options separated by space>
180 # Input variable: $first_arg_idx
181 # Result variables: $arg_idx, $choices
182 sw.Write("_ganeti_list_options() {")
185 sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
188 sw.Write("arg_idx=0")
189 # Show options only if the current word starts with a dash
190 sw.Write("""if [[ "$cur" == -* ]]; then""")
193 sw.Write("choices=$1")
202 # Calculate position of current argument
203 sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
209 # Params: <long options with equal sign> <all options>
210 # Result variable: $optcur
211 sw.Write("_ganeti_checkopt() {")
214 sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
217 sw.Write("optcur=\"${cur#--*=}\"")
221 sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
224 sw.Write("optcur=\"$cur\"")
230 sw.Write("_ganeti_dbglog optcur=\"'$optcur'\"")
237 # Params: <compgen options>
238 # Result variable: $COMPREPLY
239 sw.Write("_ganeti_compgen() {")
242 sw.Write("""COMPREPLY=( $(compgen "$@") )""")
243 sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
249 def WriteCompReply(sw, args, cur="\"$cur\""):
250 sw.Write("_ganeti_compgen %s -- %s", args, cur)
254 class CompletionWriter:
255 """Command completion writer class.
258 def __init__(self, arg_offset, opts, args):
259 self.arg_offset = arg_offset
264 # While documented, these variables aren't seen as public attributes by
265 # pylint. pylint: disable-msg=W0212
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.max is None:
469 cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
470 elif arg.min <= arg.max:
471 cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
472 (last_arg_end, last_arg_end + arg.max))
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",
491 utils.ShellQuote(" ".join(compgenargs)))
501 WriteCompReply(sw, """-W "$choices" $compgenargs""")
503 # $compgenargs exists only if there are arguments
504 WriteCompReply(sw, '-W "$choices"')
506 def WriteTo(self, sw):
507 self._FindFirstArgument(sw)
508 self._CompleteOptionValues(sw)
509 self._CompleteArguments(sw)
512 def WriteCompletion(sw, scriptname, funcname,
514 opts=None, args=None):
515 """Writes the completion code for one command.
517 @type sw: ShellWriter
518 @param sw: Script writer
519 @type scriptname: string
520 @param scriptname: Name of command line program
521 @type funcname: string
522 @param funcname: Shell function name
524 @param commands: List of all subcommands in this program
527 sw.Write("%s() {", funcname)
531 ' cur="${COMP_WORDS[COMP_CWORD]}"'
532 ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
533 ' i first_arg_idx choices compgenargs arg_idx optcur')
535 sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
536 sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
537 " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
539 sw.Write("COMPREPLY=()")
541 if opts is not None and args is not None:
543 CompletionWriter(0, opts, args).WriteTo(sw)
546 sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
549 # Complete the command name
552 utils.ShellQuote(" ".join(sorted(commands.keys())))))
557 # We're doing options and arguments to commands
558 sw.Write("""case "${COMP_WORDS[1]}" in""")
559 for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
560 if not (argdef or optdef):
563 # TODO: Group by arguments and options
564 sw.Write("%s)", utils.ShellQuote(cmd))
567 CompletionWriter(1, optdef, argdef).WriteTo(sw)
577 sw.Write("complete -F %s -o filenames %s",
578 utils.ShellQuote(funcname),
579 utils.ShellQuote(scriptname))
582 def GetFunctionName(name):
583 return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
586 def GetCommands(filename, module):
587 """Returns the commands defined in a module.
589 Aliases are also added as commands.
593 commands = getattr(module, "commands")
594 except AttributeError:
595 raise Exception("Script %s doesn't have 'commands' attribute" %
598 # Add the implicit "--help" option
599 help_option = cli.cli_option("-h", "--help", default=False,
602 for (_, _, optdef, _, _) in commands.itervalues():
603 if help_option not in optdef:
604 optdef.append(help_option)
605 if cli.DEBUG_OPT not in optdef:
606 optdef.append(cli.DEBUG_OPT)
609 aliases = getattr(module, "aliases", {})
611 commands = commands.copy()
612 for name, target in aliases.iteritems():
613 commands[name] = commands[target]
620 sw = ShellWriter(buf)
625 for scriptname in _autoconf.GNT_SCRIPTS:
626 filename = "scripts/%s" % scriptname
628 WriteCompletion(sw, scriptname,
629 GetFunctionName(scriptname),
630 commands=GetCommands(filename,
631 build.LoadModule(filename)))
634 burnin = build.LoadModule("tools/burnin")
635 WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
636 opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
641 if __name__ == "__main__":