Modify gnt-node add to call external script
[ganeti-local] / autotools / build-bash-completion
index 497db89..bcb1239 100755 (executable)
 # 02110-1301, USA.
 
 
 # 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 os
-import sys
 import re
 from cStringIO import StringIO
 
 from ganeti import constants
 from ganeti import cli
 from ganeti import utils
 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
 
 
 
 # _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.
 
 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("# 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:
   sw.Write("_ganeti_nodes() {")
   sw.IncIndent()
   try:
@@ -112,16 +102,20 @@ def WritePreamble(sw):
     sw.DecIndent()
   sw.Write("}")
 
     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
 
   # Params: <offset> <options with values> <options without values>
   # Result variable: $first_arg_idx
@@ -182,9 +176,48 @@ def WritePreamble(sw):
     sw.DecIndent()
   sw.Write("}")
 
     sw.DecIndent()
   sw.Write("}")
 
+  # Params: <long options with equal sign> <all options>
+  # Result variable: $optcur
+  sw.Write("_ganeti_checkopt() {")
+  sw.IncIndent()
+  try:
+    sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
+    sw.IncIndent()
+    try:
+      sw.Write("optcur=\"${cur#--*=}\"")
+      sw.Write("return 0")
+    finally:
+      sw.DecIndent()
+    sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
+    sw.IncIndent()
+    try:
+      sw.Write("optcur=\"$cur\"")
+      sw.Write("return 0")
+    finally:
+      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):
-  sw.Write("""COMPREPLY=( $(compgen %s -- "$cur") )""", args)
+def WriteCompReply(sw, args, cur="\"$cur\""):
+  sw.Write("_ganeti_compgen %s -- %s", args, cur)
   sw.Write("return")
 
 
   sw.Write("return")
 
 
@@ -198,6 +231,8 @@ class CompletionWriter:
     self.args = args
 
     for opt in opts:
     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):
       opt.all_names = sorted(opt._short_opts + opt._long_opts)
 
   def _FindFirstArgument(self, sw):
@@ -240,37 +275,109 @@ class CompletionWriter:
       # Only static choices implemented so far (e.g. no node list)
       suggest = getattr(opt, "completion_suggest", None)
 
       # 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 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:
       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:
       return
 
 
     # Don't write any code if there are no option values
     if not values:
       return
 
-    sw.Write("if [[ $COMP_CWORD -gt %s ]]; then", self.arg_offset + 1)
-    sw.IncIndent()
-    try:
-      sw.Write("""case "$prev" in""")
-      for (choices, names) in values.iteritems():
-        # TODO: Implement completion for --foo=bar form
-        sw.Write("%s)", "|".join([utils.ShellQuote(i) for i in names]))
-        sw.IncIndent()
-        try:
-          WriteCompReply(sw, "-W %s" % utils.ShellQuote(choices))
-        finally:
-          sw.DecIndent()
-        sw.Write(";;")
-      sw.Write("""esac""")
-    finally:
-      sw.DecIndent()
-    sw.Write("""fi""")
+    cur = "\"$optcur\""
+
+    wrote_opt = False
+
+    for (suggest, allnames) in values.iteritems():
+      longnames = [i for i in allnames if i.startswith("--")]
+
+      if wrote_opt:
+        condcmd = "elif"
+      else:
+        condcmd = "if"
+
+      sw.Write("%s _ganeti_checkopt %s %s; then", condcmd,
+               utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
+               utils.ShellQuote("|".join(allnames)))
+      sw.IncIndent()
+      try:
+        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()
+
+      wrote_opt = True
+
+    if wrote_opt:
+      sw.Write("fi")
+
+    return
 
   def _CompleteArguments(self, sw):
     if not (self.opts or self.args):
 
   def _CompleteArguments(self, sw):
     if not (self.opts or self.args):
@@ -318,6 +425,8 @@ class CompletionWriter:
           choices = "$(_ganeti_nodes)"
         elif isinstance(arg, cli.ArgJobId):
           choices = "$(_ganeti_jobs)"
           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")
         elif isinstance(arg, cli.ArgFile):
           choices = ""
           compgenargs.append("-f")
@@ -332,11 +441,11 @@ class CompletionWriter:
 
         if arg.min == 1 and arg.max == 1:
           cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
 
         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.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")
 
         else:
           raise Exception("Unable to generate argument position condition")
 
@@ -354,7 +463,8 @@ class CompletionWriter:
             if choices:
               sw.Write("""choices="$choices "%s""", choices)
             if compgenargs:
             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()
 
           finally:
             sw.DecIndent()
 
@@ -394,13 +504,13 @@ def WriteCompletion(sw, scriptname, funcname,
   sw.IncIndent()
   try:
     sw.Write("local "
   sw.IncIndent()
   try:
     sw.Write("local "
-             ' cur="${COMP_WORDS[$COMP_CWORD]}"'
+             ' cur="${COMP_WORDS[COMP_CWORD]}"'
              ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
              ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
-             ' i first_arg_idx choices compgenargs arg_idx')
+             ' 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=()")
 
 
     sw.Write("COMPREPLY=()")
 
@@ -449,19 +559,6 @@ def GetFunctionName(name):
   return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
 
 
   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.
 
 def GetCommands(filename, module):
   """Returns the commands defined in a module.
 
@@ -470,10 +567,20 @@ def GetCommands(filename, module):
   """
   try:
     commands = getattr(module, "commands")
   """
   try:
     commands = getattr(module, "commands")
-  except AttributeError, err:
+  except AttributeError:
     raise Exception("Script %s doesn't have 'commands' attribute" %
                     filename)
 
     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:
   # Use aliases
   aliases = getattr(module, "aliases", {})
   if aliases:
@@ -486,7 +593,7 @@ def GetCommands(filename, module):
 
 def main():
   buf = StringIO()
 
 def main():
   buf = StringIO()
-  sw = ShellWriter(buf)
+  sw = utils.ShellWriter(buf)
 
   WritePreamble(sw)
 
 
   WritePreamble(sw)
 
@@ -496,10 +603,11 @@ def main():
 
     WriteCompletion(sw, scriptname,
                     GetFunctionName(scriptname),
 
     WriteCompletion(sw, scriptname,
                     GetFunctionName(scriptname),
-                    commands=GetCommands(filename, LoadModule(filename)))
+                    commands=GetCommands(filename,
+                                         build.LoadModule(filename)))
 
   # Burnin script
 
   # 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)
 
   WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
                   opts=burnin.OPTIONS, args=burnin.ARGUMENTS)