Modify gnt-node add to call external script
[ganeti-local] / autotools / build-bash-completion
index 1695e84..bcb1239 100755 (executable)
 # 02110-1301, USA.
 
 
-import imp
-import optparse
+"""Script to generate bash_completion script for Ganeti.
+
+"""
+
+# pylint: disable-msg=C0103
+# [C0103] Invalid name build-bash-completion
+
 import os
-import sys
 import re
 from cStringIO import StringIO
 
 from ganeti import constants
 from ganeti import cli
 from ganeti import utils
+from ganeti import build
 
 # _autoconf shouldn't be imported from anywhere except constants.py, but we're
 # making an exception here because this script is only used at build time.
 from ganeti import _autoconf
 
 
-class ShellWriter:
-  """Helper class to write scripts with indentation.
-
-  """
-  INDENT_STR = "  "
-
-  def __init__(self, fh):
-    self._fh = fh
-    self._indent = 0
-
-  def IncIndent(self):
-    """Increase indentation level by 1.
-
-    """
-    self._indent += 1
-
-  def DecIndent(self):
-    """Decrease indentation level by 1.
-
-    """
-    assert self._indent > 0
-    self._indent -= 1
-
-  def Write(self, txt, *args):
-    """Write line to output file.
-
-    """
-    self._fh.write(self._indent * self.INDENT_STR)
-
-    if args:
-      self._fh.write(txt % args)
-    else:
-      self._fh.write(txt)
-
-    self._fh.write("\n")
-
-
 def WritePreamble(sw):
   """Writes the script preamble.
 
@@ -81,6 +49,28 @@ def WritePreamble(sw):
   sw.Write("# This script is automatically generated at build time.")
   sw.Write("# Do not modify manually.")
 
+  sw.Write("_ganeti_dbglog() {")
+  sw.IncIndent()
+  try:
+    sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
+    sw.IncIndent()
+    try:
+      sw.Write("{")
+      sw.IncIndent()
+      try:
+        sw.Write("echo ---")
+        sw.Write("echo \"$@\"")
+        sw.Write("echo")
+      finally:
+        sw.DecIndent()
+      sw.Write("} >> $GANETI_COMPL_LOG")
+    finally:
+      sw.DecIndent()
+    sw.Write("fi")
+  finally:
+    sw.DecIndent()
+  sw.Write("}")
+
   sw.Write("_ganeti_nodes() {")
   sw.IncIndent()
   try:
@@ -112,16 +102,20 @@ def WritePreamble(sw):
     sw.DecIndent()
   sw.Write("}")
 
-  sw.Write("_ganeti_os() {")
-  sw.IncIndent()
-  try:
-    # FIXME: Make querying the master for all OSes cheap
-    for path in constants.OS_SEARCH_PATH:
-      sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
-               utils.ShellQuote(path))
-  finally:
-    sw.DecIndent()
-  sw.Write("}")
+  for (fnname, paths) in [
+      ("os", constants.OS_SEARCH_PATH),
+      ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
+      ]:
+    sw.Write("_ganeti_%s() {", fnname)
+    sw.IncIndent()
+    try:
+      # FIXME: Make querying the master for all OSes cheap
+      for path in paths:
+        sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
+                 utils.ShellQuote(path))
+    finally:
+      sw.DecIndent()
+    sw.Write("}")
 
   # Params: <offset> <options with values> <options without values>
   # Result variable: $first_arg_idx
@@ -203,14 +197,27 @@ def WritePreamble(sw):
       sw.DecIndent()
     sw.Write("fi")
 
+    sw.Write("_ganeti_dbglog optcur=\"'$optcur'\"")
+
     sw.Write("return 1")
   finally:
     sw.DecIndent()
   sw.Write("}")
 
+  # Params: <compgen options>
+  # Result variable: $COMPREPLY
+  sw.Write("_ganeti_compgen() {")
+  sw.IncIndent()
+  try:
+    sw.Write("""COMPREPLY=( $(compgen "$@") )""")
+    sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
+  finally:
+    sw.DecIndent()
+  sw.Write("}")
+
 
 def WriteCompReply(sw, args, cur="\"$cur\""):
-  sw.Write("""COMPREPLY=( $(compgen %s -- %s) )""", args, cur)
+  sw.Write("_ganeti_compgen %s -- %s", args, cur)
   sw.Write("return")
 
 
@@ -224,6 +231,8 @@ class CompletionWriter:
     self.args = args
 
     for opt in opts:
+      # While documented, these variables aren't seen as public attributes by
+      # pylint. pylint: disable-msg=W0212
       opt.all_names = sorted(opt._short_opts + opt._long_opts)
 
   def _FindFirstArgument(self, sw):
@@ -266,15 +275,22 @@ class CompletionWriter:
       # Only static choices implemented so far (e.g. no node list)
       suggest = getattr(opt, "completion_suggest", None)
 
+      # our custom option type
+      if opt.type == "bool":
+        suggest = ["yes", "no"]
+
       if not suggest:
         suggest = opt.choices
 
-      if suggest:
-        suggest_text = " ".join(sorted(suggest))
+      if (isinstance(suggest, (int, long)) and
+          suggest in cli.OPT_COMPL_ALL):
+        key = suggest
+      elif suggest:
+        key = " ".join(sorted(suggest))
       else:
-        suggest_text = ""
+        key = ""
 
-      values.setdefault(suggest_text, []).extend(opt.all_names)
+      values.setdefault(key, []).extend(opt.all_names)
 
     # Don't write any code if there are no option values
     if not values:
@@ -297,7 +313,62 @@ class CompletionWriter:
                utils.ShellQuote("|".join(allnames)))
       sw.IncIndent()
       try:
-        WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
+        if suggest == cli.OPT_COMPL_MANY_NODES:
+          # TODO: Implement comma-separated values
+          WriteCompReply(sw, "-W ''", cur=cur)
+        elif suggest == cli.OPT_COMPL_ONE_NODE:
+          WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
+        elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
+          WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
+        elif suggest == cli.OPT_COMPL_ONE_OS:
+          WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
+        elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
+          WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
+        elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
+          sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
+
+          sw.Write("if [[ \"$optcur\" == *:* ]]; then")
+          sw.IncIndent()
+          try:
+            sw.Write("node1=\"${optcur%%:*}\"")
+
+            sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
+            sw.IncIndent()
+            try:
+              sw.Write("pfx=\"$node1:\"")
+            finally:
+              sw.DecIndent()
+            sw.Write("fi")
+          finally:
+            sw.DecIndent()
+          sw.Write("fi")
+
+          sw.Write("_ganeti_dbglog pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
+                   " node1=\"'$node1'\"")
+
+          sw.Write("for i in $(_ganeti_nodes); do")
+          sw.IncIndent()
+          try:
+            sw.Write("if [[ -z \"$node1\" ]]; then")
+            sw.IncIndent()
+            try:
+              sw.Write("tmp=\"$tmp $i $i:\"")
+            finally:
+              sw.DecIndent()
+            sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
+            sw.IncIndent()
+            try:
+              sw.Write("tmp=\"$tmp $i\"")
+            finally:
+              sw.DecIndent()
+            sw.Write("fi")
+          finally:
+            sw.DecIndent()
+          sw.Write("done")
+
+          WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
+        else:
+          WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
       finally:
         sw.DecIndent()
 
@@ -354,6 +425,8 @@ class CompletionWriter:
           choices = "$(_ganeti_nodes)"
         elif isinstance(arg, cli.ArgJobId):
           choices = "$(_ganeti_jobs)"
+        elif isinstance(arg, cli.ArgOs):
+          choices = "$(_ganeti_os)"
         elif isinstance(arg, cli.ArgFile):
           choices = ""
           compgenargs.append("-f")
@@ -368,11 +441,11 @@ class CompletionWriter:
 
         if arg.min == 1 and arg.max == 1:
           cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
+        elif arg.max is None:
+          cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
         elif arg.min <= arg.max:
           cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
                      (last_arg_end, last_arg_end + arg.max))
-        elif arg.max is None:
-          cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
         else:
           raise Exception("Unable to generate argument position condition")
 
@@ -390,7 +463,8 @@ class CompletionWriter:
             if choices:
               sw.Write("""choices="$choices "%s""", choices)
             if compgenargs:
-              sw.Write("compgenargs=%s", utils.ShellQuote(" ".join(compgenargs)))
+              sw.Write("compgenargs=%s",
+                       utils.ShellQuote(" ".join(compgenargs)))
           finally:
             sw.DecIndent()
 
@@ -434,9 +508,9 @@ def WriteCompletion(sw, scriptname, funcname,
              ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
              ' i first_arg_idx choices compgenargs arg_idx optcur')
 
-    # Useful for debugging:
-    #sw.Write("echo cur=\"$cur\" prev=\"$prev\"")
-    #sw.Write("set | grep ^COMP_")
+    sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
+    sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
+             " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
 
     sw.Write("COMPREPLY=()")
 
@@ -485,19 +559,6 @@ def GetFunctionName(name):
   return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
 
 
-def LoadModule(filename):
-  """Loads an external module by filename.
-
-  """
-  (name, ext) = os.path.splitext(filename)
-
-  fh = open(filename, "U")
-  try:
-    return imp.load_module(name, fh, filename, (ext, "U", imp.PY_SOURCE))
-  finally:
-    fh.close()
-
-
 def GetCommands(filename, module):
   """Returns the commands defined in a module.
 
@@ -506,10 +567,20 @@ def GetCommands(filename, module):
   """
   try:
     commands = getattr(module, "commands")
-  except AttributeError, err:
+  except AttributeError:
     raise Exception("Script %s doesn't have 'commands' attribute" %
                     filename)
 
+  # Add the implicit "--help" option
+  help_option = cli.cli_option("-h", "--help", default=False,
+                               action="store_true")
+
+  for (_, _, optdef, _, _) in commands.itervalues():
+    if help_option not in optdef:
+      optdef.append(help_option)
+    if cli.DEBUG_OPT not in optdef:
+      optdef.append(cli.DEBUG_OPT)
+
   # Use aliases
   aliases = getattr(module, "aliases", {})
   if aliases:
@@ -522,7 +593,7 @@ def GetCommands(filename, module):
 
 def main():
   buf = StringIO()
-  sw = ShellWriter(buf)
+  sw = utils.ShellWriter(buf)
 
   WritePreamble(sw)
 
@@ -532,10 +603,11 @@ def main():
 
     WriteCompletion(sw, scriptname,
                     GetFunctionName(scriptname),
-                    commands=GetCommands(filename, LoadModule(filename)))
+                    commands=GetCommands(filename,
+                                         build.LoadModule(filename)))
 
   # Burnin script
-  burnin = LoadModule("tools/burnin")
+  burnin = build.LoadModule("tools/burnin")
   WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
                   opts=burnin.OPTIONS, args=burnin.ARGUMENTS)