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'\"")
240 def WriteCompReply(sw, args, cur="\"$cur\""):
241 sw.Write("""COMPREPLY=( $(compgen %s -- %s) )""", args, cur)
242 sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
246 class CompletionWriter:
247 """Command completion writer class.
250 def __init__(self, arg_offset, opts, args):
251 self.arg_offset = arg_offset
256 opt.all_names = sorted(opt._short_opts + opt._long_opts)
258 def _FindFirstArgument(self, sw):
262 for opt in self.opts:
263 if opt.takes_value():
265 for i in opt.all_names:
266 if i.startswith("--"):
267 ignore.append("%s=*" % utils.ShellQuote(i))
268 skip_one.append(utils.ShellQuote(i))
270 ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
272 ignore = sorted(utils.UniqueSequence(ignore))
273 skip_one = sorted(utils.UniqueSequence(skip_one))
275 if ignore or skip_one:
276 # Try to locate first argument
277 sw.Write("_ganeti_find_first_arg %s %s %s",
279 utils.ShellQuote("|".join(skip_one)),
280 utils.ShellQuote("|".join(ignore)))
282 # When there are no options the first argument is always at position
284 sw.Write("first_arg_idx=%s", self.arg_offset + 1)
286 def _CompleteOptionValues(self, sw):
288 # "values" -> [optname1, optname2, ...]
291 for opt in self.opts:
292 if not opt.takes_value():
295 # Only static choices implemented so far (e.g. no node list)
296 suggest = getattr(opt, "completion_suggest", None)
299 suggest = opt.choices
301 if (isinstance(suggest, (int, long)) and
302 suggest in cli.OPT_COMPL_ALL):
305 key = " ".join(sorted(suggest))
309 values.setdefault(key, []).extend(opt.all_names)
311 # Don't write any code if there are no option values
319 for (suggest, allnames) in values.iteritems():
320 longnames = [i for i in allnames if i.startswith("--")]
327 sw.Write("%s _ganeti_checkopt %s %s; then", condcmd,
328 utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
329 utils.ShellQuote("|".join(allnames)))
332 if suggest == cli.OPT_COMPL_MANY_NODES:
333 # TODO: Implement comma-separated values
334 WriteCompReply(sw, "-W ''", cur=cur)
335 elif suggest == cli.OPT_COMPL_ONE_NODE:
336 WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
337 elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
338 WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
339 elif suggest == cli.OPT_COMPL_ONE_OS:
340 WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
341 elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
342 WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
343 elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
344 sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
346 sw.Write("if [[ \"$optcur\" == *:* ]]; then")
349 sw.Write("node1=\"${optcur%%:*}\"")
351 sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
354 sw.Write("pfx=\"$node1:\"")
362 sw.Write("_ganeti_dbglog pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
363 " node1=\"'$node1'\"")
365 sw.Write("for i in $(_ganeti_nodes); do")
368 sw.Write("if [[ -z \"$node1\" ]]; then")
371 sw.Write("tmp=\"$tmp $i $i:\"")
374 sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
377 sw.Write("tmp=\"$tmp $i\"")
385 WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
387 WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
398 def _CompleteArguments(self, sw):
399 if not (self.opts or self.args):
402 all_option_names = []
403 for opt in self.opts:
404 all_option_names.extend(opt.all_names)
405 all_option_names.sort()
407 # List options if no argument has been specified yet
408 sw.Write("_ganeti_list_options %s",
409 utils.ShellQuote(" ".join(all_option_names)))
412 last_idx = len(self.args) - 1
414 varlen_arg_idx = None
417 # Write some debug comments
418 for idx, arg in enumerate(self.args):
419 sw.Write("# %s: %r", idx, arg)
421 sw.Write("compgenargs=")
423 for idx, arg in enumerate(self.args):
424 assert arg.min is not None and arg.min >= 0
425 assert not (idx < last_idx and arg.max is None)
427 if arg.min != arg.max or arg.max is None:
428 if varlen_arg_idx is not None:
429 raise Exception("Only one argument can have a variable length")
434 if isinstance(arg, cli.ArgUnknown):
436 elif isinstance(arg, cli.ArgSuggest):
437 choices = utils.ShellQuote(" ".join(arg.choices))
438 elif isinstance(arg, cli.ArgInstance):
439 choices = "$(_ganeti_instances)"
440 elif isinstance(arg, cli.ArgNode):
441 choices = "$(_ganeti_nodes)"
442 elif isinstance(arg, cli.ArgJobId):
443 choices = "$(_ganeti_jobs)"
444 elif isinstance(arg, cli.ArgFile):
446 compgenargs.append("-f")
447 elif isinstance(arg, cli.ArgCommand):
449 compgenargs.append("-c")
450 elif isinstance(arg, cli.ArgHost):
452 compgenargs.append("-A hostname")
454 raise Exception("Unknown argument type %r" % arg)
456 if arg.min == 1 and arg.max == 1:
457 cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
458 elif arg.min <= arg.max:
459 cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
460 (last_arg_end, last_arg_end + arg.max))
461 elif arg.max is None:
462 cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
464 raise Exception("Unable to generate argument position condition")
466 last_arg_end += arg.min
468 if choices or compgenargs:
474 sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
478 sw.Write("""choices="$choices "%s""", choices)
480 sw.Write("compgenargs=%s", utils.ShellQuote(" ".join(compgenargs)))
490 WriteCompReply(sw, """-W "$choices" $compgenargs""")
492 # $compgenargs exists only if there are arguments
493 WriteCompReply(sw, '-W "$choices"')
495 def WriteTo(self, sw):
496 self._FindFirstArgument(sw)
497 self._CompleteOptionValues(sw)
498 self._CompleteArguments(sw)
501 def WriteCompletion(sw, scriptname, funcname,
503 opts=None, args=None):
504 """Writes the completion code for one command.
506 @type sw: ShellWriter
507 @param sw: Script writer
508 @type scriptname: string
509 @param scriptname: Name of command line program
510 @type funcname: string
511 @param funcname: Shell function name
513 @param commands: List of all subcommands in this program
516 sw.Write("%s() {", funcname)
520 ' cur="${COMP_WORDS[COMP_CWORD]}"'
521 ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
522 ' i first_arg_idx choices compgenargs arg_idx optcur')
524 sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
525 sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
526 " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
528 sw.Write("COMPREPLY=()")
530 if opts is not None and args is not None:
532 CompletionWriter(0, opts, args).WriteTo(sw)
535 sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
538 # Complete the command name
541 utils.ShellQuote(" ".join(sorted(commands.keys())))))
546 # We're doing options and arguments to commands
547 sw.Write("""case "${COMP_WORDS[1]}" in""")
548 for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
549 if not (argdef or optdef):
552 # TODO: Group by arguments and options
553 sw.Write("%s)", utils.ShellQuote(cmd))
556 CompletionWriter(1, optdef, argdef).WriteTo(sw)
566 sw.Write("complete -F %s -o filenames %s",
567 utils.ShellQuote(funcname),
568 utils.ShellQuote(scriptname))
571 def GetFunctionName(name):
572 return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
575 def GetCommands(filename, module):
576 """Returns the commands defined in a module.
578 Aliases are also added as commands.
582 commands = getattr(module, "commands")
583 except AttributeError, err:
584 raise Exception("Script %s doesn't have 'commands' attribute" %
587 # Add the implicit "--help" option
588 help_option = cli.cli_option("-h", "--help", default=False,
591 for (_, _, optdef, _, _) in commands.itervalues():
592 if help_option not in optdef:
593 optdef.append(help_option)
594 if cli.DEBUG_OPT not in optdef:
595 optdef.append(cli.DEBUG_OPT)
598 aliases = getattr(module, "aliases", {})
600 commands = commands.copy()
601 for name, target in aliases.iteritems():
602 commands[name] = commands[target]
609 sw = ShellWriter(buf)
614 for scriptname in _autoconf.GNT_SCRIPTS:
615 filename = "scripts/%s" % scriptname
617 WriteCompletion(sw, scriptname,
618 GetFunctionName(scriptname),
619 commands=GetCommands(filename,
620 build.LoadModule(filename)))
623 burnin = build.LoadModule("tools/burnin")
624 WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
625 opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
630 if __name__ == "__main__":