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