diff --git a/.gitignore b/.gitignore
index 11fb23dc8fc1ac1750c9cefecdf5b6426b12a4f4..850bcc30202216742c75ad7babae1f5b3e9b8dbb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,7 @@
 /doc/rapi-resources.sgml
 
 # doc/examples
+/doc/examples/bash_completion
 /doc/examples/ganeti.cron
 /doc/examples/ganeti.initd
 
diff --git a/Makefile.am b/Makefile.am
index 5aeeff903ce2f2a462dd6220ca1e8a4306930342..17f6518ca61781b6ccbaf9a65fbb9016ea66789d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -228,13 +228,14 @@ TESTS_ENVIRONMENT = PYTHONPATH=.:$(top_builddir)
 
 
 all-local: stamp-directories lib/_autoconf.py devel/upload \
+	doc/examples/bash_completion \
 	doc/examples/ganeti.initd doc/examples/ganeti.cron
 
 devel/upload: devel/upload.in stamp-directories $(REPLACE_VARS_SED)
 	sed -f $(REPLACE_VARS_SED) < $< > $@
 	chmod u+x $@
 
-doc/examples/ganeti.%: doc/examples/ganeti.%.in stamp-directories \
+doc/examples/%: doc/examples/%.in stamp-directories \
 		$(REPLACE_VARS_SED)
 	sed -f $(REPLACE_VARS_SED) < $< > $@
 
diff --git a/doc/examples/bash_completion.in b/doc/examples/bash_completion.in
new file mode 100644
index 0000000000000000000000000000000000000000..7e43ed84e232758031547f0984a62be59dacf1e6
--- /dev/null
+++ b/doc/examples/bash_completion.in
@@ -0,0 +1,537 @@
+_gnt_backup()
+{
+  local cur prev base_cmd cmds ilist nlist
+  COMPREPLY=()
+  cur="$2"
+  prev="$3"
+  #
+  #  The basic options we'll complete.
+  #
+  cmds="export import list remove"
+
+  # default completion is empty
+  COMPREPLY=()
+
+  if [[ ! -f "@LOCALSTATEDIR@/lib/ganeti/ssconf_cluster_name" ]]; then
+    # cluster not initialized
+    return 0
+  fi
+
+  ilist=$(< "@LOCALSTATEDIR@/lib/ganeti/ssconf_instance_list")
+  nlist=$(< "@LOCALSTATEDIR@/lib/ganeti/ssconf_node_list")
+
+  case "$COMP_CWORD" in
+    1)
+      # complete the command name
+      COMPREPLY=( $(compgen -W "$cmds" -- ${cur}) )
+      ;;
+    *)
+      # we're doing options to commands
+      base_cmd="${COMP_WORDS[1]}"
+      case "${base_cmd}" in
+        export)
+          case "$COMP_CWORD" in
+            2)
+              # options or instances
+              COMPREPLY=( $(compgen -W "--no-shutdown -n $ilist" -- ${cur}) )
+              ;;
+            3)
+              # if previous was option, we allow instance
+              case "$prev" in
+                -*)
+                  COMPREPLY=( $(compgen -W "$ilist" -- ${cur}) )
+                  ;;
+              esac
+          esac
+          ;;
+        import)
+          case "$prev" in
+            -t)
+              COMPREPLY=( $(compgen -W "diskless file plain drbd" -- ${cur}) )
+              ;;
+            --src-node)
+              COMPREPLY=( $(compgen -W "$nlist" -- ${cur}) )
+              ;;
+            --file-driver)
+              COMPREPLY=( $(compgen -W "loop blktap" -- ${cur}) )
+              ;;
+            -*)
+              # arguments to other options, we don't have completion yet
+              ;;
+            *)
+              COMPREPLY=( $(compgen -W "-t -n -B -H -s --disks --net \
+                              --no-nics --no-start --no-ip-check -I \
+                              --src-node --src-dir --file-driver \
+                              --file-storage-dir" -- ${cur}) )
+              ;;
+          esac
+          ;;
+        remove)
+          if [[ "$COMP_CWORD" -eq 2 ]]; then
+            COMPREPLY=( $(compgen -W "$ilist" -- ${cur}) )
+          fi
+          ;;
+      esac
+  esac
+
+  return 0
+}
+
+complete -F _gnt_backup gnt-backup
+
+_gnt_cluster()
+{
+  local cur prev cmds
+  cur="$2"
+  prev="$3"
+  #
+  #  The basic options we'll complete.
+  #
+  if [[ -e "@LOCALSTATEDIR@/lib/ganeti/ssconf_cluster_name" ]]; then
+    cmds="add-tags command copyfile destroy getmaster info list-tags \
+          masterfailover modify queue redist-conf remove-tags rename \
+          search-tags verify verify-disks version"
+  else
+    cmds="init"
+  fi
+
+  # default completion is empty
+  COMPREPLY=()
+  case "$COMP_CWORD" in
+    1)
+      # complete the command name
+      COMPREPLY=($(compgen -W "$cmds" -- ${cur}))
+      ;;
+    2)
+      # complete arguments to the command
+      case "$prev" in
+        "queue")
+        COMPREPLY=( $(compgen -W "drain undrain info" -- ${cur}) )
+        ;;
+      *)
+        ;;
+      esac
+  esac
+
+  return 0
+}
+
+complete -F _gnt_cluster gnt-cluster
+
+_gnt_debug()
+{
+  local cur prev cmds
+  cur="$2"
+  prev="$3"
+
+  cmds="allocator delay submit-job"
+
+  # default completion is empty
+  COMPREPLY=()
+
+  if [[ ! -f "@LOCALSTATEDIR@/lib/ganeti/ssconf_cluster_name" ]]; then
+    # cluster not initialized
+    return 0
+  fi
+
+  case "$COMP_CWORD" in
+    1)
+      # complete the command name
+      COMPREPLY=( $(compgen -W "$cmds" -- ${cur}) )
+      ;;
+    *)
+      # we're doing options to commands
+      base_cmd="${COMP_WORDS[1]}"
+      case "${base_cmd}" in
+        delay)
+          if [[ "$prev" != -* ]]; then
+            COMPREPLY=( $(compgen -W "--no-master -n" -- ${cur}) )
+          fi
+          ;;
+        submit-job)
+          if [[ "$COMP_CWORD" -eq 2 ]]; then
+            COMPREPLY=( $(compgen -f -- ${cur}) )
+          fi
+          ;;
+      esac
+  esac
+
+  return 0
+}
+
+complete -F _gnt_debug gnt-debug
+
+_gnt_instance()
+{
+  local cur prev base_cmd cmds ilist nlist
+  COMPREPLY=()
+  cur="$2"
+  prev="$3"
+  #
+  #  The basic options we'll complete.
+  #
+  cmds="activate-disks add add-tags batch-create console deactivate-disks \
+        failover grow-disk info list list-tags migrate modify reboot \
+        reinstall remove remove-tags rename replace-disks shutdown startup"
+
+  # default completion is empty
+  COMPREPLY=()
+
+  if [[ ! -f "@LOCALSTATEDIR@/lib/ganeti/ssconf_cluster_name" ]]; then
+    # cluster not initialized
+    return 0
+  fi
+
+  ilist=$(< "@LOCALSTATEDIR@/lib/ganeti/ssconf_instance_list")
+  nlist=$(< "@LOCALSTATEDIR@/lib/ganeti/ssconf_node_list")
+
+  case "$COMP_CWORD" in
+    1)
+      # complete the command name
+      COMPREPLY=( $(compgen -W "$cmds" -- ${cur}) )
+      ;;
+    *)
+      # we're doing options to commands
+      base_cmd="${COMP_WORDS[1]}"
+      case "${base_cmd}" in
+        # first, rules for multiple commands
+        activate-disks|console|deactivate-disks|list-tags|rename|remove)
+          # commands with only one instance argument, nothing else
+          if [[ "$COMP_CWORD" -eq 2 ]]; then
+            COMPREPLY=( $(compgen -W "$ilist" -- ${cur}) )
+          fi
+          ;;
+        info)
+          # commands with more than one instance
+          COMPREPLY=( $(compgen -W "$ilist" -- ${cur}) )
+          ;;
+        add-tags|grow-disk|reinstall|remove-tags|replace-disks)
+          # not very well handled
+          COMPREPLY=( $(compgen -W "$ilist" -- ${cur}) )
+          ;;
+        startup|start|shutdown|stop|reboot)
+          COMPREPLY=( $(compgen -W "--force-multiple --node --primary \
+                          --secondary --all --submit $ilist" -- ${cur}) )
+          ;;
+        # individual commands
+        add)
+          case "$prev" in
+            -t)
+              COMPREPLY=( $(compgen -W "diskless file plain drbd" -- ${cur}) )
+              ;;
+            --file-driver)
+              COMPREPLY=( $(compgen -W "loop blktap" -- ${cur}) )
+              ;;
+            -*)
+              # arguments to other options, we don't have completion yet
+              ;;
+            *)
+              COMPREPLY=( $(compgen -W "-t -n -o -B -H -s --disks --net \
+                            --no-nics --no-start --no-ip-check -I \
+                            --file-driver --file-storage-dir --submit" \
+                -- ${cur}) )
+              ;;
+          esac
+          ;;
+        batch-create)
+          # this only takes one file name
+          COMPREPLY=( $(compgen -A file -- ${cur}) )
+          ;;
+        failover)
+          case "$COMP_CWORD" in
+            2)
+              # options or instances
+              COMPREPLY=( $(compgen -W "--ignore-failures $ilist" -- ${cur}) )
+              ;;
+            3)
+              # if previous was option, we allow instance
+              case "$prev" in
+                -*)
+                  COMPREPLY=( $(compgen -W "$ilist" -- ${cur}) )
+                  ;;
+              esac
+          esac
+          ;;
+        list)
+          COMPREPLY=( $(compgen -W "--no-headers --separator --units -o \
+                          --sync $ilist" -- ${cur}) )
+          ;;
+        modify)
+          COMPREPLY=( $(compgen -W "-H -B --disk --net $ilist" -- ${cur}) )
+          ;;
+        migrate)
+          case "$COMP_CWORD" in
+            2)
+              # options or instances
+              COMPREPLY=( $(compgen -W "--non-live --cleanup $ilist" -- \
+                ${cur}) )
+              ;;
+            3)
+              # if previous was option, we allow instance
+              case "$prev" in
+                -*)
+                  COMPREPLY=( $(compgen -W "$ilist" -- ${cur}) )
+                  ;;
+              esac
+          esac
+      esac
+  esac
+
+  return 0
+}
+
+complete -F _gnt_instance gnt-instance
+
+_gnt_job()
+{
+  local cur prev cmds
+  cur="$2"
+  prev="$3"
+
+  cmds="archive autoarchive cancel info list"
+
+  # default completion is empty
+  COMPREPLY=()
+
+  if [[ ! -f "@LOCALSTATEDIR@/lib/ganeti/ssconf_cluster_name" ]]; then
+    # cluster not initialized
+    return 0
+  fi
+
+  case "$COMP_CWORD" in
+    1)
+      # complete the command name
+      COMPREPLY=( $(compgen -W "$cmds" -- ${cur}) )
+      ;;
+    *)
+      # we're doing options to commands
+      base_cmd="${COMP_WORDS[1]}"
+      case "${base_cmd}" in
+        archive|cancel|info)
+          # FIXME: this is really going into the internals of the job queue
+          jlist=$( cd @LOCALSTATEDIR@/lib/ganeti/queue; echo job-*)
+          jlist=${jlist//job-/}
+          COMPREPLY=( $(compgen -W "$jlist" -- ${cur}) )
+          ;;
+        list)
+          COMPREPLY=( $(compgen -W "--no-headers --separator -o" -- ${cur}) )
+          ;;
+
+      esac
+  esac
+
+  return 0
+}
+
+complete -F _gnt_job gnt-job
+
+_gnt_os()
+{
+  local cur prev cmds
+  cur="$2"
+  prev="$3"
+
+  cmds="list diagnose"
+
+  # default completion is empty
+  COMPREPLY=()
+
+  if [[ ! -f "@LOCALSTATEDIR@/lib/ganeti/ssconf_cluster_name" ]]; then
+    # cluster not initialized
+    return 0
+  fi
+
+  case "$COMP_CWORD" in
+    1)
+      # complete the command name
+      COMPREPLY=( $(compgen -W "$cmds" -- ${cur}) )
+      ;;
+    *)
+      # we're doing options to commands
+      base_cmd="${COMP_WORDS[1]}"
+      case "${base_cmd}" in
+        list)
+          if [[ "$COMP_CWORD" -eq 2 ]]; then
+            COMPREPLY=( $(compgen -W "--no-headers" -- ${cur}) )
+          fi
+          ;;
+      esac
+  esac
+
+  return 0
+}
+
+complete -F _gnt_os gnt-os
+
+_gnt_node()
+{
+  local cur prev cmds base_cmd
+  cur="$2"
+  prev="$3"
+
+  cmds="add add-tags evacuate failover info list list-tags migrate modify \
+          remove remove-tags volumes"
+
+  # default completion is empty
+  COMPREPLY=()
+
+  if [[ ! -f "@LOCALSTATEDIR@/lib/ganeti/ssconf_cluster_name" ]]; then
+    # cluster not initialized
+    return 0
+  fi
+
+  nlist=$(< "@LOCALSTATEDIR@/lib/ganeti/ssconf_node_list")
+
+  case "$COMP_CWORD" in
+    1)
+      # complete the command name
+      COMPREPLY=( $(compgen -W "$cmds" -- ${cur}) )
+      ;;
+    *)
+      # we're doing options to commands
+      base_cmd="${COMP_WORDS[1]}"
+      case "${base_cmd}" in
+        # first rules for multiple commands
+        list-tags|remove)
+          # commands with only one instance argument, nothing else
+          if [[ "$COMP_CWORD" -eq 2 ]]; then
+            COMPREPLY=( $(compgen -W "$nlist" -- ${cur}) )
+          fi
+          ;;
+        add-tags|info|remove-tags|volumes)
+          COMPREPLY=( $(compgen -W "$nlist" -- ${cur}) )
+          ;;
+        # individual commands
+        add)
+          # options or instances
+          COMPREPLY=( $(compgen -W "-s --readd --no-ssh-key-check" -- ${cur}) )
+          ;;
+        evacuate)
+          case "$COMP_CWORD" in
+            2)
+              # options or instances
+              COMPREPLY=( $(compgen -W "-n -I $nlist" -- ${cur}) )
+              ;;
+            3)
+              # if previous was option, we allow node
+              case "$prev" in
+                -*)
+                  COMPREPLY=( $(compgen -W "$nlist" -- ${cur}) )
+                  ;;
+              esac
+          esac
+          ;;
+        failover)
+          case "$COMP_CWORD" in
+            2)
+              # options or instances
+              COMPREPLY=( $(compgen -W "--ignore-failures $nlist" -- ${cur}) )
+              ;;
+            3)
+              # if previous was option, we allow node
+              case "$prev" in
+                -*)
+                  COMPREPLY=( $(compgen -W "$nlist" -- ${cur}) )
+                  ;;
+              esac
+          esac
+          ;;
+        list)
+          COMPREPLY=( $(compgen -W "--no-headers --separator --units -o \
+                          --sync $nlist" -- ${cur}) )
+          ;;
+        modify)
+          # TODO: after a non-option, don't allow options
+          if [[ "$COMP_CWORD" -eq 2 || "$prev" != -* ]]; then
+            COMPREPLY=( $(compgen -W "-C -O -D $nlist" -- ${cur}) )
+          elif [[ "$prev" == -* ]]; then
+            COMPREPLY=( $(compgen -W "yes no" -- ${cur}) )
+          fi
+          ;;
+        migrate)
+          case "$COMP_CWORD" in
+            2)
+              # options or nodes
+              COMPREPLY=( $(compgen -W "--non-live $nlist" -- ${cur}) )
+              ;;
+            3)
+              # if previous was option, we allow node
+              case "$prev" in
+                -*)
+                  COMPREPLY=( $(compgen -W "$nlist" -- ${cur}) )
+                  ;;
+              esac
+          esac
+      esac
+  esac
+
+  return 0
+}
+
+complete -F _gnt_node gnt-node
+
+# other tools
+
+_gnt_tool_burnin()
+{
+  local cur prev
+  cur="$2"
+  prev="$3"
+
+  # default completion is empty
+  COMPREPLY=()
+
+  if [[ ! -f "@LOCALSTATEDIR@/lib/ganeti/ssconf_cluster_name" ]]; then
+    # cluster not initialized
+    return 0
+  fi
+
+  nlist=$(< "@LOCALSTATEDIR@/lib/ganeti/ssconf_node_list")
+
+  case "$prev" in
+    -t)
+      COMPREPLY=( $(compgen -W "diskless file plain drbd" -- ${cur}) )
+      ;;
+    --rename)
+      # this needs an unused host name, so we don't complete it
+      ;;
+    -n|--nodes)
+      # nodes from the cluster, comma separated
+      # FIXME: make completion work for comma-separated values
+      COMPREPLY=( $(compgen -W "$nlist" -- ${cur}) )
+      ;;
+    -o|--os)
+      # the os list
+      COMPREPLY=( $(compgen -W "$(gnt-os list --no-headers)" -- ${cur}) )
+      ;;
+    --disk-size|--disk-growth)
+      # these take a number or unit, we can't really autocomplete, but
+      # we show a couple of examples
+      COMPREPLY=( $(compgen -W "128M 512M 1G 4G 1G,256M 4G,1G,1G 10G" -- \
+        ${cur}) )
+      ;;
+    --mem-size)
+      # this takes a number or unit, we can't really autocomplete, but
+      # we show a couple of examples
+      COMPREPLY=( $(compgen -W "128M 256M 512M 1G 4G 8G 12G 16G" -- ${cur}) )
+      ;;
+    --net-timeout)
+      # this takes a number in seconds; again, we can't really complete
+      COMPREPLY=( $(compgen -W "15 60 300 900" -- ${cur}) )
+      ;;
+    *)
+      # all other, we just list the whole options
+      COMPREPLY=( $(compgen -W "-o --disk-size --disk-growth --mem-size \
+                      -v --verbose --no-replace1 --no-replace2 --no-failover \
+                      --no-migrate --no-importexport --no-startstop \
+                      --no-reinstall --no-reboot --no-activate-disks \
+                      --no-add-disks --no-add-nics --no-nics \
+                      --rename -t -n --nodes -I --iallocator -p --parallel \
+                      --net-timeout -C --http-check -K --keep-instances" \
+        -- ${cur}) )
+  esac
+
+  return 0
+}
+
+complete -F _gnt_tool_burnin @PREFIX@/lib/ganeti/tools/burnin