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