Adding QA RAPI tests for activate-disks and deactivate-disks calls
[ganeti-local] / autotools / build-bash-completion
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2009 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """Script to generate bash_completion script for Ganeti.
23
24 """
25
26 # pylint: disable-msg=C0103
27 # [C0103] Invalid name build-bash-completion
28
29 import os
30 import re
31 from cStringIO import StringIO
32
33 from ganeti import constants
34 from ganeti import cli
35 from ganeti import utils
36 from ganeti import build
37
38 # _autoconf shouldn't be imported from anywhere except constants.py, but we're
39 # making an exception here because this script is only used at build time.
40 from ganeti import _autoconf
41
42
43 class ShellWriter:
44   """Helper class to write scripts with indentation.
45
46   """
47   INDENT_STR = "  "
48
49   def __init__(self, fh):
50     self._fh = fh
51     self._indent = 0
52
53   def IncIndent(self):
54     """Increase indentation level by 1.
55
56     """
57     self._indent += 1
58
59   def DecIndent(self):
60     """Decrease indentation level by 1.
61
62     """
63     assert self._indent > 0
64     self._indent -= 1
65
66   def Write(self, txt, *args):
67     """Write line to output file.
68
69     """
70     self._fh.write(self._indent * self.INDENT_STR)
71
72     if args:
73       self._fh.write(txt % args)
74     else:
75       self._fh.write(txt)
76
77     self._fh.write("\n")
78
79
80 def WritePreamble(sw):
81   """Writes the script preamble.
82
83   Helper functions should be written here.
84
85   """
86   sw.Write("# This script is automatically generated at build time.")
87   sw.Write("# Do not modify manually.")
88
89   sw.Write("_ganeti_dbglog() {")
90   sw.IncIndent()
91   try:
92     sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
93     sw.IncIndent()
94     try:
95       sw.Write("{")
96       sw.IncIndent()
97       try:
98         sw.Write("echo ---")
99         sw.Write("echo \"$@\"")
100         sw.Write("echo")
101       finally:
102         sw.DecIndent()
103       sw.Write("} >> $GANETI_COMPL_LOG")
104     finally:
105       sw.DecIndent()
106     sw.Write("fi")
107   finally:
108     sw.DecIndent()
109   sw.Write("}")
110
111   sw.Write("_ganeti_nodes() {")
112   sw.IncIndent()
113   try:
114     node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list")
115     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
116   finally:
117     sw.DecIndent()
118   sw.Write("}")
119
120   sw.Write("_ganeti_instances() {")
121   sw.IncIndent()
122   try:
123     instance_list_path = os.path.join(constants.DATA_DIR,
124                                       "ssconf_instance_list")
125     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
126   finally:
127     sw.DecIndent()
128   sw.Write("}")
129
130   sw.Write("_ganeti_jobs() {")
131   sw.IncIndent()
132   try:
133     # FIXME: this is really going into the internals of the job queue
134     sw.Write(("local jlist=$( shopt -s nullglob &&"
135               " cd %s 2>/dev/null && echo job-* || : )"),
136              utils.ShellQuote(constants.QUEUE_DIR))
137     sw.Write('echo "${jlist//job-/}"')
138   finally:
139     sw.DecIndent()
140   sw.Write("}")
141
142   for (fnname, paths) in [
143       ("os", constants.OS_SEARCH_PATH),
144       ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
145       ]:
146     sw.Write("_ganeti_%s() {", fnname)
147     sw.IncIndent()
148     try:
149       # FIXME: Make querying the master for all OSes cheap
150       for path in paths:
151         sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
152                  utils.ShellQuote(path))
153     finally:
154       sw.DecIndent()
155     sw.Write("}")
156
157   # Params: <offset> <options with values> <options without values>
158   # Result variable: $first_arg_idx
159   sw.Write("_ganeti_find_first_arg() {")
160   sw.IncIndent()
161   try:
162     sw.Write("local w i")
163
164     sw.Write("first_arg_idx=")
165     sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
166     sw.IncIndent()
167     try:
168       sw.Write("w=${COMP_WORDS[$i]}")
169
170       # Skip option value
171       sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
172
173       # Skip
174       sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
175
176       # Ah, we found the first argument
177       sw.Write("else first_arg_idx=$i; break;")
178       sw.Write("fi")
179     finally:
180       sw.DecIndent()
181     sw.Write("done")
182   finally:
183     sw.DecIndent()
184   sw.Write("}")
185
186   # Params: <list of options separated by space>
187   # Input variable: $first_arg_idx
188   # Result variables: $arg_idx, $choices
189   sw.Write("_ganeti_list_options() {")
190   sw.IncIndent()
191   try:
192     sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
193     sw.IncIndent()
194     try:
195       sw.Write("arg_idx=0")
196       # Show options only if the current word starts with a dash
197       sw.Write("""if [[ "$cur" == -* ]]; then""")
198       sw.IncIndent()
199       try:
200         sw.Write("choices=$1")
201       finally:
202         sw.DecIndent()
203       sw.Write("fi")
204       sw.Write("return")
205     finally:
206       sw.DecIndent()
207     sw.Write("fi")
208
209     # Calculate position of current argument
210     sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
211     sw.Write("choices=")
212   finally:
213     sw.DecIndent()
214   sw.Write("}")
215
216   # Params: <long options with equal sign> <all options>
217   # Result variable: $optcur
218   sw.Write("_ganeti_checkopt() {")
219   sw.IncIndent()
220   try:
221     sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
222     sw.IncIndent()
223     try:
224       sw.Write("optcur=\"${cur#--*=}\"")
225       sw.Write("return 0")
226     finally:
227       sw.DecIndent()
228     sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
229     sw.IncIndent()
230     try:
231       sw.Write("optcur=\"$cur\"")
232       sw.Write("return 0")
233     finally:
234       sw.DecIndent()
235     sw.Write("fi")
236
237     sw.Write("_ganeti_dbglog optcur=\"'$optcur'\"")
238
239     sw.Write("return 1")
240   finally:
241     sw.DecIndent()
242   sw.Write("}")
243
244   # Params: <compgen options>
245   # Result variable: $COMPREPLY
246   sw.Write("_ganeti_compgen() {")
247   sw.IncIndent()
248   try:
249     sw.Write("""COMPREPLY=( $(compgen "$@") )""")
250     sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
251   finally:
252     sw.DecIndent()
253   sw.Write("}")
254
255
256 def WriteCompReply(sw, args, cur="\"$cur\""):
257   sw.Write("_ganeti_compgen %s -- %s", args, cur)
258   sw.Write("return")
259
260
261 class CompletionWriter:
262   """Command completion writer class.
263
264   """
265   def __init__(self, arg_offset, opts, args):
266     self.arg_offset = arg_offset
267     self.opts = opts
268     self.args = args
269
270     for opt in opts:
271       # While documented, these variables aren't seen as public attributes by
272       # pylint. pylint: disable-msg=W0212
273       opt.all_names = sorted(opt._short_opts + opt._long_opts)
274
275   def _FindFirstArgument(self, sw):
276     ignore = []
277     skip_one = []
278
279     for opt in self.opts:
280       if opt.takes_value():
281         # Ignore value
282         for i in opt.all_names:
283           if i.startswith("--"):
284             ignore.append("%s=*" % utils.ShellQuote(i))
285           skip_one.append(utils.ShellQuote(i))
286       else:
287         ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
288
289     ignore = sorted(utils.UniqueSequence(ignore))
290     skip_one = sorted(utils.UniqueSequence(skip_one))
291
292     if ignore or skip_one:
293       # Try to locate first argument
294       sw.Write("_ganeti_find_first_arg %s %s %s",
295                self.arg_offset + 1,
296                utils.ShellQuote("|".join(skip_one)),
297                utils.ShellQuote("|".join(ignore)))
298     else:
299       # When there are no options the first argument is always at position
300       # offset + 1
301       sw.Write("first_arg_idx=%s", self.arg_offset + 1)
302
303   def _CompleteOptionValues(self, sw):
304     # Group by values
305     # "values" -> [optname1, optname2, ...]
306     values = {}
307
308     for opt in self.opts:
309       if not opt.takes_value():
310         continue
311
312       # Only static choices implemented so far (e.g. no node list)
313       suggest = getattr(opt, "completion_suggest", None)
314
315       # our custom option type
316       if opt.type == "bool":
317         suggest = ["yes", "no"]
318
319       if not suggest:
320         suggest = opt.choices
321
322       if (isinstance(suggest, (int, long)) and
323           suggest in cli.OPT_COMPL_ALL):
324         key = suggest
325       elif suggest:
326         key = " ".join(sorted(suggest))
327       else:
328         key = ""
329
330       values.setdefault(key, []).extend(opt.all_names)
331
332     # Don't write any code if there are no option values
333     if not values:
334       return
335
336     cur = "\"$optcur\""
337
338     wrote_opt = False
339
340     for (suggest, allnames) in values.iteritems():
341       longnames = [i for i in allnames if i.startswith("--")]
342
343       if wrote_opt:
344         condcmd = "elif"
345       else:
346         condcmd = "if"
347
348       sw.Write("%s _ganeti_checkopt %s %s; then", condcmd,
349                utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
350                utils.ShellQuote("|".join(allnames)))
351       sw.IncIndent()
352       try:
353         if suggest == cli.OPT_COMPL_MANY_NODES:
354           # TODO: Implement comma-separated values
355           WriteCompReply(sw, "-W ''", cur=cur)
356         elif suggest == cli.OPT_COMPL_ONE_NODE:
357           WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
358         elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
359           WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
360         elif suggest == cli.OPT_COMPL_ONE_OS:
361           WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
362         elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
363           WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
364         elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
365           sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
366
367           sw.Write("if [[ \"$optcur\" == *:* ]]; then")
368           sw.IncIndent()
369           try:
370             sw.Write("node1=\"${optcur%%:*}\"")
371
372             sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
373             sw.IncIndent()
374             try:
375               sw.Write("pfx=\"$node1:\"")
376             finally:
377               sw.DecIndent()
378             sw.Write("fi")
379           finally:
380             sw.DecIndent()
381           sw.Write("fi")
382
383           sw.Write("_ganeti_dbglog pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
384                    " node1=\"'$node1'\"")
385
386           sw.Write("for i in $(_ganeti_nodes); do")
387           sw.IncIndent()
388           try:
389             sw.Write("if [[ -z \"$node1\" ]]; then")
390             sw.IncIndent()
391             try:
392               sw.Write("tmp=\"$tmp $i $i:\"")
393             finally:
394               sw.DecIndent()
395             sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
396             sw.IncIndent()
397             try:
398               sw.Write("tmp=\"$tmp $i\"")
399             finally:
400               sw.DecIndent()
401             sw.Write("fi")
402           finally:
403             sw.DecIndent()
404           sw.Write("done")
405
406           WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
407         else:
408           WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
409       finally:
410         sw.DecIndent()
411
412       wrote_opt = True
413
414     if wrote_opt:
415       sw.Write("fi")
416
417     return
418
419   def _CompleteArguments(self, sw):
420     if not (self.opts or self.args):
421       return
422
423     all_option_names = []
424     for opt in self.opts:
425       all_option_names.extend(opt.all_names)
426     all_option_names.sort()
427
428     # List options if no argument has been specified yet
429     sw.Write("_ganeti_list_options %s",
430              utils.ShellQuote(" ".join(all_option_names)))
431
432     if self.args:
433       last_idx = len(self.args) - 1
434       last_arg_end = 0
435       varlen_arg_idx = None
436       wrote_arg = False
437
438       # Write some debug comments
439       for idx, arg in enumerate(self.args):
440         sw.Write("# %s: %r", idx, arg)
441
442       sw.Write("compgenargs=")
443
444       for idx, arg in enumerate(self.args):
445         assert arg.min is not None and arg.min >= 0
446         assert not (idx < last_idx and arg.max is None)
447
448         if arg.min != arg.max or arg.max is None:
449           if varlen_arg_idx is not None:
450             raise Exception("Only one argument can have a variable length")
451           varlen_arg_idx = idx
452
453         compgenargs = []
454
455         if isinstance(arg, cli.ArgUnknown):
456           choices = ""
457         elif isinstance(arg, cli.ArgSuggest):
458           choices = utils.ShellQuote(" ".join(arg.choices))
459         elif isinstance(arg, cli.ArgInstance):
460           choices = "$(_ganeti_instances)"
461         elif isinstance(arg, cli.ArgNode):
462           choices = "$(_ganeti_nodes)"
463         elif isinstance(arg, cli.ArgJobId):
464           choices = "$(_ganeti_jobs)"
465         elif isinstance(arg, cli.ArgOs):
466           choices = "$(_ganeti_os)"
467         elif isinstance(arg, cli.ArgFile):
468           choices = ""
469           compgenargs.append("-f")
470         elif isinstance(arg, cli.ArgCommand):
471           choices = ""
472           compgenargs.append("-c")
473         elif isinstance(arg, cli.ArgHost):
474           choices = ""
475           compgenargs.append("-A hostname")
476         else:
477           raise Exception("Unknown argument type %r" % arg)
478
479         if arg.min == 1 and arg.max == 1:
480           cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
481         elif arg.max is None:
482           cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
483         elif arg.min <= arg.max:
484           cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
485                      (last_arg_end, last_arg_end + arg.max))
486         else:
487           raise Exception("Unable to generate argument position condition")
488
489         last_arg_end += arg.min
490
491         if choices or compgenargs:
492           if wrote_arg:
493             condcmd = "elif"
494           else:
495             condcmd = "if"
496
497           sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
498           sw.IncIndent()
499           try:
500             if choices:
501               sw.Write("""choices="$choices "%s""", choices)
502             if compgenargs:
503               sw.Write("compgenargs=%s",
504                        utils.ShellQuote(" ".join(compgenargs)))
505           finally:
506             sw.DecIndent()
507
508           wrote_arg = True
509
510       if wrote_arg:
511         sw.Write("fi")
512
513     if self.args:
514       WriteCompReply(sw, """-W "$choices" $compgenargs""")
515     else:
516       # $compgenargs exists only if there are arguments
517       WriteCompReply(sw, '-W "$choices"')
518
519   def WriteTo(self, sw):
520     self._FindFirstArgument(sw)
521     self._CompleteOptionValues(sw)
522     self._CompleteArguments(sw)
523
524
525 def WriteCompletion(sw, scriptname, funcname,
526                     commands=None,
527                     opts=None, args=None):
528   """Writes the completion code for one command.
529
530   @type sw: ShellWriter
531   @param sw: Script writer
532   @type scriptname: string
533   @param scriptname: Name of command line program
534   @type funcname: string
535   @param funcname: Shell function name
536   @type commands: list
537   @param commands: List of all subcommands in this program
538
539   """
540   sw.Write("%s() {", funcname)
541   sw.IncIndent()
542   try:
543     sw.Write("local "
544              ' cur="${COMP_WORDS[COMP_CWORD]}"'
545              ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
546              ' i first_arg_idx choices compgenargs arg_idx optcur')
547
548     sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
549     sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
550              " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
551
552     sw.Write("COMPREPLY=()")
553
554     if opts is not None and args is not None:
555       assert not commands
556       CompletionWriter(0, opts, args).WriteTo(sw)
557
558     else:
559       sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
560       sw.IncIndent()
561       try:
562         # Complete the command name
563         WriteCompReply(sw,
564                        ("-W %s" %
565                         utils.ShellQuote(" ".join(sorted(commands.keys())))))
566       finally:
567         sw.DecIndent()
568       sw.Write("fi")
569
570       # We're doing options and arguments to commands
571       sw.Write("""case "${COMP_WORDS[1]}" in""")
572       for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
573         if not (argdef or optdef):
574           continue
575
576         # TODO: Group by arguments and options
577         sw.Write("%s)", utils.ShellQuote(cmd))
578         sw.IncIndent()
579         try:
580           CompletionWriter(1, optdef, argdef).WriteTo(sw)
581         finally:
582           sw.DecIndent()
583
584         sw.Write(";;")
585       sw.Write("esac")
586   finally:
587     sw.DecIndent()
588   sw.Write("}")
589
590   sw.Write("complete -F %s -o filenames %s",
591            utils.ShellQuote(funcname),
592            utils.ShellQuote(scriptname))
593
594
595 def GetFunctionName(name):
596   return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
597
598
599 def GetCommands(filename, module):
600   """Returns the commands defined in a module.
601
602   Aliases are also added as commands.
603
604   """
605   try:
606     commands = getattr(module, "commands")
607   except AttributeError:
608     raise Exception("Script %s doesn't have 'commands' attribute" %
609                     filename)
610
611   # Add the implicit "--help" option
612   help_option = cli.cli_option("-h", "--help", default=False,
613                                action="store_true")
614
615   for (_, _, optdef, _, _) in commands.itervalues():
616     if help_option not in optdef:
617       optdef.append(help_option)
618     if cli.DEBUG_OPT not in optdef:
619       optdef.append(cli.DEBUG_OPT)
620
621   # Use aliases
622   aliases = getattr(module, "aliases", {})
623   if aliases:
624     commands = commands.copy()
625     for name, target in aliases.iteritems():
626       commands[name] = commands[target]
627
628   return commands
629
630
631 def main():
632   buf = StringIO()
633   sw = ShellWriter(buf)
634
635   WritePreamble(sw)
636
637   # gnt-* scripts
638   for scriptname in _autoconf.GNT_SCRIPTS:
639     filename = "scripts/%s" % scriptname
640
641     WriteCompletion(sw, scriptname,
642                     GetFunctionName(scriptname),
643                     commands=GetCommands(filename,
644                                          build.LoadModule(filename)))
645
646   # Burnin script
647   burnin = build.LoadModule("tools/burnin")
648   WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
649                   opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
650
651   print buf.getvalue()
652
653
654 if __name__ == "__main__":
655   main()