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