Merge branch 'devel-2.4'
authorMichael Hanselmann <hansmi@google.com>
Mon, 25 Jul 2011 13:02:23 +0000 (15:02 +0200)
committerMichael Hanselmann <hansmi@google.com>
Mon, 25 Jul 2011 13:06:56 +0000 (15:06 +0200)
* devel-2.4:
  Reopen daemon's stdio on SIGHUP
  Reopen log file only once after SIGHUP
  Don't leak file descriptors when setting up daemon output
  Fix aliases in bash completion

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Iustin Pop <iustin@google.com>

180 files changed:
.gitignore
INSTALL
Makefile.am
NEWS
UPGRADE
autotools/convert-constants [new file with mode: 0755]
autotools/docpp [copied from test/ganeti.hypervisor.hv_fake_unittest.py with 60% similarity]
autotools/run-in-tempdir
configure.ac
daemons/ensure-dirs.in [deleted file]
doc/admin.rst
doc/conf.py
doc/design-2.0.rst
doc/design-2.3.rst
doc/design-chained-jobs.rst [new file with mode: 0644]
doc/design-cpu-pinning.rst [new file with mode: 0644]
doc/design-draft.rst [new file with mode: 0644]
doc/design-htools-2.3.rst [new file with mode: 0644]
doc/design-http-server.rst [new file with mode: 0644]
doc/design-impexp2.rst [new file with mode: 0644]
doc/design-lu-generated-jobs.rst [new file with mode: 0644]
doc/design-multi-reloc.rst [new file with mode: 0644]
doc/design-network.rst [new file with mode: 0644]
doc/design-oob.rst
doc/design-ovf-support.rst [new file with mode: 0644]
doc/design-query2.rst
doc/design-shared-storage.rst [new file with mode: 0644]
doc/design-x509-ca.rst [new file with mode: 0644]
doc/devnotes.rst
doc/hooks.rst
doc/iallocator.rst
doc/index.rst
doc/install.rst
doc/rapi.rst
doc/walkthrough.rst
htools/Ganeti/Constants.hs.in [new file with mode: 0644]
htools/Ganeti/HTools/CLI.hs [new file with mode: 0644]
htools/Ganeti/HTools/Cluster.hs [new file with mode: 0644]
htools/Ganeti/HTools/Compat.hs [new file with mode: 0644]
htools/Ganeti/HTools/Container.hs [new file with mode: 0644]
htools/Ganeti/HTools/ExtLoader.hs [new file with mode: 0644]
htools/Ganeti/HTools/Group.hs [new file with mode: 0644]
htools/Ganeti/HTools/IAlloc.hs [new file with mode: 0644]
htools/Ganeti/HTools/Instance.hs [new file with mode: 0644]
htools/Ganeti/HTools/Loader.hs [new file with mode: 0644]
htools/Ganeti/HTools/Luxi.hs [new file with mode: 0644]
htools/Ganeti/HTools/Node.hs [new file with mode: 0644]
htools/Ganeti/HTools/PeerMap.hs [new file with mode: 0644]
htools/Ganeti/HTools/Program/Hail.hs [new file with mode: 0644]
htools/Ganeti/HTools/Program/Hbal.hs [new file with mode: 0644]
htools/Ganeti/HTools/Program/Hscan.hs [new file with mode: 0644]
htools/Ganeti/HTools/Program/Hspace.hs [new file with mode: 0644]
htools/Ganeti/HTools/QC.hs [new file with mode: 0644]
htools/Ganeti/HTools/Rapi.hs [new file with mode: 0644]
htools/Ganeti/HTools/Simu.hs [new file with mode: 0644]
htools/Ganeti/HTools/Text.hs [new file with mode: 0644]
htools/Ganeti/HTools/Types.hs [new file with mode: 0644]
htools/Ganeti/HTools/Utils.hs [new file with mode: 0644]
htools/Ganeti/HTools/Version.hs.in [new file with mode: 0644]
htools/Ganeti/Jobs.hs [new file with mode: 0644]
htools/Ganeti/Luxi.hs [new file with mode: 0644]
htools/Ganeti/OpCodes.hs [new file with mode: 0644]
htools/OLD-NEWS [new file with mode: 0644]
htools/README [new file with mode: 0644]
htools/haddock-prologue [new file with mode: 0644]
htools/htools.hs [new file with mode: 0644]
htools/live-test.sh [new file with mode: 0755]
htools/test.hs [new file with mode: 0644]
lib/backend.py
lib/bdev.py
lib/bootstrap.py
lib/build/sphinx_ext.py [new file with mode: 0644]
lib/cache.py [new file with mode: 0644]
lib/cli.py
lib/client/gnt_cluster.py
lib/client/gnt_debug.py
lib/client/gnt_group.py
lib/client/gnt_instance.py
lib/client/gnt_job.py
lib/client/gnt_node.py
lib/client/gnt_os.py
lib/cmdlib.py
lib/config.py
lib/constants.py
lib/errors.py
lib/ht.py
lib/http/client.py
lib/hypervisor/hv_base.py
lib/hypervisor/hv_chroot.py
lib/hypervisor/hv_fake.py
lib/hypervisor/hv_kvm.py
lib/hypervisor/hv_lxc.py
lib/hypervisor/hv_xen.py
lib/jqueue.py
lib/jstore.py
lib/locking.py
lib/luxi.py
lib/mcpu.py
lib/netutils.py
lib/objects.py
lib/opcodes.py
lib/qlang.py
lib/query.py
lib/rapi/baserlib.py
lib/rapi/client.py
lib/rapi/connector.py
lib/rapi/rlib2.py
lib/rpc.py
lib/server/masterd.py
lib/server/noded.py
lib/ssconf.py
lib/tools/__init__.py [copied from lib/client/__init__.py with 90% similarity]
lib/tools/ensure_dirs.py [new file with mode: 0644]
lib/utils/log.py
lib/utils/nodesetup.py
lib/utils/process.py
lib/utils/retry.py
lib/utils/text.py
lib/watcher/__init__.py
man/footer.rst
man/ganeti-cleaner.rst
man/ganeti-confd.rst
man/ganeti-listrunner.rst
man/ganeti-masterd.rst
man/ganeti-noded.rst
man/ganeti-os-interface.rst
man/ganeti-rapi.rst
man/ganeti-watcher.rst
man/ganeti.rst
man/gnt-backup.rst
man/gnt-cluster.rst
man/gnt-debug.rst
man/gnt-group.rst
man/gnt-instance.rst
man/gnt-job.rst
man/gnt-node.rst
man/gnt-os.rst
man/hail.rst [new file with mode: 0644]
man/hbal.rst [new file with mode: 0644]
man/hscan.rst [new file with mode: 0644]
man/hspace.rst [new file with mode: 0644]
man/htools.rst [new file with mode: 0644]
qa/ganeti-qa.py
qa/qa-sample.json
qa/qa_cluster.py
qa/qa_group.py
qa/qa_instance.py
qa/qa_node.py
qa/qa_os.py
qa/qa_rapi.py
qa/qa_tags.py
qa/qa_utils.py
test/docs_unittest.py
test/ganeti.cache_unittest.py [new file with mode: 0755]
test/ganeti.cli_unittest.py
test/ganeti.client.gnt_cluster_unittest.py [new file with mode: 0755]
test/ganeti.cmdlib_unittest.py
test/ganeti.hooks_unittest.py
test/ganeti.ht_unittest.py
test/ganeti.hypervisor.hv_kvm_unittest.py
test/ganeti.jqueue_unittest.py
test/ganeti.locking_unittest.py
test/ganeti.opcodes_unittest.py
test/ganeti.qlang_unittest.py
test/ganeti.query_unittest.py
test/ganeti.rapi.baserlib_unittest.py
test/ganeti.rapi.client_unittest.py
test/ganeti.rapi.rlib2_unittest.py
test/ganeti.tools.ensure_dirs_unittest.py [new file with mode: 0755]
test/ganeti.utils.nodesetup_unittest.py
test/ganeti.utils.process_unittest.py
test/ganeti.utils.retry_unittest.py
test/ganeti.utils.text_unittest.py
test/ganeti.utils.x509_unittest.py
test/mocks.py
tools/burnin
tools/cfgupgrade
tools/kvm-console-wrapper [new file with mode: 0755]
tools/lvmstrap
tools/xm-console-wrapper [copied from test/ganeti.hypervisor.hv_fake_unittest.py with 52% similarity]

index ebb7325..fa26229 100644 (file)
@@ -8,6 +8,9 @@
 *.py[co]
 *.swp
 *~
+*.o
+*.hi
+*.hp
 .dir
 
 # /
@@ -35,7 +38,6 @@
 
 # daemons
 /daemons/daemon-util
-/daemons/ensure-dirs
 /daemons/ganeti-cleaner
 /daemons/ganeti-confd
 /daemons/ganeti-masterd
 /devel/upload
 
 # doc
-/doc/api
+/doc/api/
 /doc/build
-/doc/coverage
+/doc/coverage/
 /doc/html
 /doc/install-quick.rst
 /doc/news.rst
 /doc/upgrade.rst
+/doc/hs-lint.html
 /doc/*.in
 /doc/*.png
 
 /man/*.[0-9]
 /man/*.html
 /man/*.in
+/man/*.gen
 /man/footer.man
 
 # tools
 /tools/kvm-ifup
+/tools/ensure-dirs
 
 # scripts
 /scripts/gnt-backup
 /scripts/gnt-job
 /scripts/gnt-node
 /scripts/gnt-os
+
+# htools-specific rules
+/htools/apidoc
+/htools/.hpc
+/htools/coverage
+
+/htools/htools
+/htools/test
+/htools/*.prof*
+/htools/*.stat
+/htools/*.tix
+/.hpc/
+
+/htools/Ganeti/HTools/Version.hs
+/htools/Ganeti/Constants.hs
diff --git a/INSTALL b/INSTALL
index 0c44f75..cb56e80 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -28,7 +28,8 @@ Before installing, please verify that you have the following programs:
 - `Python <http://www.python.org/>`_, version 2.4 or above, not 3.0
 - `Python OpenSSL bindings <http://pyopenssl.sourceforge.net/>`_
 - `simplejson Python module <http://code.google.com/p/simplejson/>`_
-- `pyparsing Python module <http://pyparsing.wikispaces.com/>`_
+- `pyparsing Python module <http://pyparsing.wikispaces.com/>`_, version
+  1.4.6 or above
 - `pyinotify Python module <http://trac.dbzteam.org/pyinotify/>`_
 - `PycURL Python module <http://pycurl.sourceforge.net/>`_
 - `ctypes Python module
@@ -50,6 +51,40 @@ packages, except for DRBD and Xen::
                     python-pyparsing python-simplejson \
                     python-pyinotify python-pycurl socat
 
+If you want to also enable the `htools` components, which is recommended
+on bigger deployments (they give you automatic instance placement,
+cluster balancing, etc.), then you need to have a Haskell compiler
+installed. More specifically:
+
+- `GHC <http://www.haskell.org/ghc/>`_ version 6.10 or higher
+- or even better, `The Haskell Platform
+  <http://hackage.haskell.org/platform/>`_ which gives you a simple way
+  to bootstrap Haskell
+- `json <http://hackage.haskell.org/package/json>`_, a JSON library
+- `network <http://hackage.haskell.org/package/network>`_, a basic
+  network library
+- `parallel <http://hackage.haskell.org/package/parallel>`_, a parallel
+  programming library (note: tested with up to version 3.x)
+- `curl <http://hackage.haskell.org/package/curl>`_, bindings for the
+  curl library, only needed if you want these tools to connect to remote
+  clusters (as opposed to the local one)
+
+All of these are also available as package in Debian/Ubuntu::
+
+  $ apt-get install ghc6 libghc6-json-dev libghc6-network-dev \
+                    libghc6-parallel-dev libghc6-curl-dev
+
+Note that more recent version have switched to GHC 7.x and the packages
+were renamed::
+
+  $ apt-get install ghc libghc-json-dev libghc-network-dev \
+                    libghc-parallel-dev libghc-curl-dev
+
+The compilation of the htools components is automatically enabled when
+the compiler and the requisite libraries are found. You can use the
+``--enable-htools`` configure flag to force the selection (at which
+point ``./configure`` will fail if it doesn't find the prerequisites).
+
 If you want to build from source, please see doc/devnotes.rst for more
 dependencies.
 
index 95733e0..38a335c 100644 (file)
@@ -19,8 +19,12 @@ CHECK_PYTHON_CODE = $(top_srcdir)/autotools/check-python-code
 CHECK_MAN = $(top_srcdir)/autotools/check-man
 CHECK_VERSION = $(top_srcdir)/autotools/check-version
 CHECK_NEWS = $(top_srcdir)/autotools/check-news
+DOCPP = $(top_srcdir)/autotools/docpp
 REPLACE_VARS_SED = autotools/replace_vars.sed
+CONVERT_CONSTANTS = $(top_srcdir)/autotools/convert-constants
 
+# Note: these are automake-specific variables, and must be named after
+# the directory + 'dir' suffix
 clientdir = $(pkgpythondir)/client
 hypervisordir = $(pkgpythondir)/hypervisor
 httpdir = $(pkgpythondir)/http
@@ -32,11 +36,19 @@ watcherdir = $(pkgpythondir)/watcher
 impexpddir = $(pkgpythondir)/impexpd
 utilsdir = $(pkgpythondir)/utils
 toolsdir = $(pkglibdir)/tools
+iallocatorsdir = $(pkglibdir)/iallocators
+pytoolsdir = $(pkgpythondir)/tools
 docdir = $(datadir)/doc/$(PACKAGE)
 
 # Delete output file if an error occurred while building it
 .DELETE_ON_ERROR:
 
+HTOOLS_DIRS = \
+       htools \
+       htools/Ganeti \
+       htools/Ganeti/HTools \
+       htools/Ganeti/HTools/Program
+
 DIRS = \
        autotools \
        daemons \
@@ -45,6 +57,7 @@ DIRS = \
        doc/examples \
        doc/examples/hooks \
        doc/examples/gnt-debug \
+       $(HTOOLS_DIRS) \
        lib \
        lib/client \
        lib/build \
@@ -55,6 +68,7 @@ DIRS = \
        lib/masterd \
        lib/rapi \
        lib/server \
+       lib/tools \
        lib/utils \
        lib/watcher \
        man \
@@ -65,8 +79,15 @@ DIRS = \
 
 BUILDTIME_DIR_AUTOCREATE = \
        scripts \
-       doc/api \
-       doc/coverage
+       $(APIDOC_DIR) \
+       $(APIDOC_PY_DIR) \
+       $(APIDOC_HS_DIR) \
+       $(APIDOC_HS_DIR)/Ganeti $(APIDOC_HS_DIR)/Ganeti/HTools \
+       $(APIDOC_HS_DIR)/Ganeti/HTools/Program \
+       $(COVERAGE_DIR) \
+       $(COVERAGE_PY_DIR) \
+       $(COVERAGE_HS_DIR) \
+       .hpc
 
 BUILDTIME_DIRS = \
        $(BUILDTIME_DIR_AUTOCREATE) \
@@ -79,6 +100,14 @@ DIRCHECK_EXCLUDE = \
 
 all_dirfiles = $(addsuffix /.dir,$(DIRS) $(BUILDTIME_DIR_AUTOCREATE))
 
+# some helper vars
+COVERAGE_DIR = doc/coverage
+COVERAGE_PY_DIR = $(COVERAGE_DIR)/py
+COVERAGE_HS_DIR = $(COVERAGE_DIR)/hs
+APIDOC_DIR = doc/api
+APIDOC_PY_DIR = $(APIDOC_DIR)/py
+APIDOC_HS_DIR = $(APIDOC_DIR)/hs
+
 MAINTAINERCLEANFILES = \
        $(docpng) \
        $(maninput) \
@@ -92,12 +121,13 @@ maintainer-clean-local:
 
 CLEANFILES = \
        $(addsuffix /*.py[co],$(DIRS)) \
+       $(addsuffix /*.hi,$(HTOOLS_DIRS)) \
+       $(addsuffix /*.o,$(HTOOLS_DIRS)) \
        $(all_dirfiles) \
        $(PYTHON_BOOTSTRAP) \
        epydoc.conf \
        autotools/replace_vars.sed \
        daemons/daemon-util \
-       daemons/ensure-dirs \
        daemons/ganeti-cleaner \
        devel/upload \
        doc/examples/bash_completion \
@@ -110,7 +140,10 @@ CLEANFILES = \
        $(manhtml) \
        tools/kvm-ifup \
        stamp-srclinks \
-       $(nodist_pkgpython_PYTHON)
+       $(nodist_pkgpython_PYTHON) \
+       $(HS_ALL_PROGS) $(HS_BUILT_SRCS) \
+       .hpc/*.mix htools/*.tix \
+       doc/hs-lint.html
 
 # BUILT_SOURCES should only be used as a dependency on phony targets. Otherwise
 # it'll cause the target to rebuild every time.
@@ -125,7 +158,8 @@ nodist_pkgpython_PYTHON = \
        lib/_autoconf.py
 
 noinst_PYTHON = \
-       lib/build/__init__.py
+       lib/build/__init__.py \
+       lib/build/sphinx_ext.py
 
 pkgpython_PYTHON = \
        lib/__init__.py \
@@ -217,6 +251,10 @@ server_PYTHON = \
        lib/server/noded.py \
        lib/server/rapi.py
 
+pytools_PYTHON = \
+       lib/tools/__init__.py \
+       lib/tools/ensure_dirs.py
+
 utils_PYTHON = \
        lib/utils/__init__.py \
        lib/utils/algo.py \
@@ -238,10 +276,21 @@ docrst = \
        doc/design-2.1.rst \
        doc/design-2.2.rst \
        doc/design-2.3.rst \
+       doc/design-htools-2.3.rst \
        doc/design-2.4.rst \
+       doc/design-draft.rst \
        doc/design-oob.rst \
+       doc/design-cpu-pinning.rst \
        doc/design-query2.rst \
+       doc/design-x509-ca.rst \
+       doc/design-http-server.rst \
+       doc/design-impexp2.rst \
+       doc/design-lu-generated-jobs.rst \
+       doc/design-multi-reloc.rst \
+       doc/design-network.rst \
+       doc/design-chained-jobs.rst \
        doc/cluster-merge.rst \
+       doc/design-shared-storage.rst \
        doc/devnotes.rst \
        doc/glossary.rst \
        doc/hooks.rst \
@@ -257,10 +306,60 @@ docrst = \
        doc/upgrade.rst \
        doc/walkthrough.rst
 
+HS_PROGS = htools/htools
+HS_BIN_ROLES = hbal hscan hspace
+
+HS_ALL_PROGS = $(HS_PROGS) htools/test
+HS_PROG_SRCS = $(patsubst %,%.hs,$(HS_ALL_PROGS))
+# we don't add -Werror by default
+HFLAGS = -O -Wall -fwarn-monomorphism-restriction -fwarn-tabs -ihtools
+# extra flags that can be overriden on the command line
+HEXTRA =
+# exclude options for coverage reports
+HPCEXCL = --exclude Main --exclude Ganeti.HTools.QC \
+       --exclude Ganeti.Constants \
+       --exclude Ganeti.HTools.Version
+
+HS_LIB_SRCS = \
+       htools/Ganeti/HTools/CLI.hs \
+       htools/Ganeti/HTools/Cluster.hs \
+       htools/Ganeti/HTools/Compat.hs \
+       htools/Ganeti/HTools/Container.hs \
+       htools/Ganeti/HTools/ExtLoader.hs \
+       htools/Ganeti/HTools/Group.hs \
+       htools/Ganeti/HTools/IAlloc.hs \
+       htools/Ganeti/HTools/Instance.hs \
+       htools/Ganeti/HTools/Loader.hs \
+       htools/Ganeti/HTools/Luxi.hs \
+       htools/Ganeti/HTools/Node.hs \
+       htools/Ganeti/HTools/PeerMap.hs \
+       htools/Ganeti/HTools/QC.hs \
+       htools/Ganeti/HTools/Rapi.hs \
+       htools/Ganeti/HTools/Simu.hs \
+       htools/Ganeti/HTools/Text.hs \
+       htools/Ganeti/HTools/Types.hs \
+       htools/Ganeti/HTools/Utils.hs \
+       htools/Ganeti/HTools/Program/Hail.hs \
+       htools/Ganeti/HTools/Program/Hbal.hs \
+       htools/Ganeti/HTools/Program/Hscan.hs \
+       htools/Ganeti/HTools/Program/Hspace.hs \
+       htools/Ganeti/Jobs.hs \
+       htools/Ganeti/Luxi.hs \
+       htools/Ganeti/OpCodes.hs
+
+HS_BUILT_SRCS = htools/Ganeti/HTools/Version.hs htools/Ganeti/Constants.hs
+HS_BUILT_SRCS_IN = $(patsubst %,%.in,$(HS_BUILT_SRCS))
+
 $(RUN_IN_TEMPDIR): | $(all_dirfiles)
 
+# Note: we use here an order-only prerequisite, as the contents of
+# _autoconf.py are not actually influencing the html build output: it
+# has to exist in order for the sphinx module to be loaded
+# successfully, but we certainly don't want the docs to be rebuilt if
+# it changes
 doc/html/index.html: $(docrst) $(docpng) doc/conf.py configure.ac \
-       $(RUN_IN_TEMPDIR)
+       $(RUN_IN_TEMPDIR) lib/build/sphinx_ext.py lib/opcodes.py lib/ht.py \
+       | lib/_autoconf.py
        @test -n "$(SPHINX)" || \
            { echo 'sphinx-build' not found during configure; exit 1; }
        @mkdir_p@ $(dir $@)
@@ -316,7 +415,7 @@ gnt_scripts = \
        scripts/gnt-node \
        scripts/gnt-os
 
-PYTHON_BOOTSTRAP = \
+PYTHON_BOOTSTRAP_SBIN = \
        daemons/ganeti-confd \
        daemons/ganeti-masterd \
        daemons/ganeti-noded \
@@ -331,6 +430,10 @@ PYTHON_BOOTSTRAP = \
        scripts/gnt-node \
        scripts/gnt-os
 
+PYTHON_BOOTSTRAP = \
+       $(PYTHON_BOOTSTRAP_SBIN) \
+       tools/ensure-dirs
+
 qa_scripts = \
        qa/ganeti-qa.py \
        qa/qa_cluster.py \
@@ -345,14 +448,47 @@ qa_scripts = \
        qa/qa_tags.py \
        qa/qa_utils.py
 
+bin_SCRIPTS =
+if WANT_HTOOLS
+bin_SCRIPTS += $(filter-out htools/hail,$(HS_PROGS))
+install-exec-hook:
+       @mkdir_p@ $(DESTDIR)$(iallocatorsdir)
+       $(LN_S) -f $(DESTDIR)$(bindir)/htools \
+                  $(DESTDIR)$(iallocatorsdir)/hail
+       for role in $(HS_BIN_ROLES); do \
+               $(LN_S) -f $(DESTDIR)$(bindir)/htools \
+                          $(DESTDIR)$(bindir)/$$role ; \
+       done
+endif
+
+$(HS_ALL_PROGS): %: %.hs $(HS_LIB_SRCS) $(HS_BUILT_SRCS) Makefile
+       @if [ -z "$(HTOOLS)" ]; then \
+         echo "Error: htools compilation disabled at configure time" 1>&2 ;\
+         exit 1; \
+       fi
+       @BINARY=$(@:htools/%=%); \
+       if [ "$BINARY" = "test" ] && [ -z "$(GHC_PKG_QUICKCHECK)" ]; then \
+         echo "Error: cannot run unittests without the QuickCheck library (see devnotes.rst)" 1>&2; \
+         exit 1; \
+       fi
+       BINARY=$(@:htools/%=%); $(GHC) --make \
+         $(HFLAGS) $(HEXTRA) \
+         $(HTOOLS_NOCURL) $(HTOOLS_PARALLEL3) \
+         -osuf $$BINARY.o -hisuf $$BINARY.hi $@
+
+# for the htools/test binary, we need to enable profiling/coverage
+htools/test: HEXTRA=-fhpc -Wwarn -fno-warn-missing-signatures \
+       -fno-warn-monomorphism-restriction -fno-warn-orphans \
+       -fno-warn-missing-methods -fno-warn-unused-imports
+
 dist_sbin_SCRIPTS = \
        tools/ganeti-listrunner
 
 nodist_sbin_SCRIPTS = \
-       $(PYTHON_BOOTSTRAP) \
+       $(PYTHON_BOOTSTRAP_SBIN) \
        daemons/ganeti-cleaner
 
-dist_tools_SCRIPTS = \
+dist_tools_PYTHON = \
        tools/burnin \
        tools/cfgshell \
        tools/cfgupgrade \
@@ -363,16 +499,26 @@ dist_tools_SCRIPTS = \
        tools/setup-ssh \
        tools/sanitize-config
 
+dist_tools_SCRIPTS = \
+       $(dist_tools_PYTHON) \
+       tools/kvm-console-wrapper \
+       tools/xm-console-wrapper
+
 pkglib_python_scripts = \
        daemons/import-export \
        tools/check-cert-expired
 
+nodist_pkglib_python_scripts = \
+       tools/ensure-dirs
+
 pkglib_SCRIPTS = \
        daemons/daemon-util \
-       daemons/ensure-dirs \
        tools/kvm-ifup \
        $(pkglib_python_scripts)
 
+nodist_pkglib_SCRIPTS = \
+       $(nodist_pkglib_python_scripts)
+
 EXTRA_DIST = \
        NEWS \
        UPGRADE \
@@ -384,11 +530,12 @@ EXTRA_DIST = \
        autotools/check-news \
        autotools/check-tar \
        autotools/check-version \
+       autotools/convert-constants \
+       autotools/docpp \
        autotools/gen-coverage \
        autotools/testrunner \
        $(RUN_IN_TEMPDIR) \
        daemons/daemon-util.in \
-       daemons/ensure-dirs.in \
        daemons/ganeti-cleaner.in \
        $(pkglib_python_scripts) \
        devel/upload.in \
@@ -417,7 +564,9 @@ EXTRA_DIST = \
        $(manrst) \
        $(maninput) \
        qa/qa-sample.json \
-       $(qa_scripts)
+       $(qa_scripts) \
+       $(HS_LIB_SRCS) $(HS_BUILT_SRCS_IN) \
+       $(HS_PROG_SRCS)
 
 man_MANS = \
        man/ganeti.7 \
@@ -436,14 +585,20 @@ man_MANS = \
        man/gnt-instance.8 \
        man/gnt-job.8 \
        man/gnt-node.8 \
-       man/gnt-os.8
-
-manrst = $(patsubst %.7,%.rst,$(patsubst %.8,%.rst,$(man_MANS)))
+       man/gnt-os.8 \
+       man/hail.1 \
+       man/hbal.1 \
+       man/hscan.1 \
+       man/hspace.1 \
+       man/htools.1
+
+manrst = $(patsubst %.1,%.rst,$(patsubst %.7,%.rst,$(patsubst %.8,%.rst,$(man_MANS))))
 manhtml = $(patsubst %.rst,%.html,$(manrst))
+mangen = $(patsubst %.rst,%.gen,$(manrst))
 maninput = \
-       $(patsubst %.7,%.7.in,$(patsubst %.8,%.8.in,$(man_MANS))) \
+       $(patsubst %.1,%.1.in,$(patsubst %.7,%.7.in,$(patsubst %.8,%.8.in,$(man_MANS)))) \
        $(patsubst %.html,%.html.in,$(manhtml)) \
-       man/footer.man man/footer.html
+       man/footer.man man/footer.html $(mangen)
 
 TEST_FILES = \
        test/data/bdev-drbd-8.0.txt \
@@ -463,6 +618,7 @@ python_tests = \
        test/ganeti.backend_unittest.py \
        test/ganeti.bdev_unittest.py \
        test/ganeti.cli_unittest.py \
+       test/ganeti.client.gnt_cluster_unittest.py \
        test/ganeti.client.gnt_instance_unittest.py \
        test/ganeti.daemon_unittest.py \
        test/ganeti.cmdlib_unittest.py \
@@ -499,6 +655,7 @@ python_tests = \
        test/ganeti.runtime_unittest.py \
        test/ganeti.serializer_unittest.py \
        test/ganeti.ssh_unittest.py \
+       test/ganeti.tools.ensure_dirs_unittest.py \
        test/ganeti.uidpool_unittest.py \
        test/ganeti.utils.algo_unittest.py \
        test/ganeti.utils.filelock_unittest.py \
@@ -518,6 +675,8 @@ python_tests = \
        test/docs_unittest.py \
        test/tempfile_fork_unittest.py
 
+haskell_tests = htools/test
+
 dist_TESTS = \
        test/check-cert-expired_unittest.bash \
        test/daemon-util_unittest.bash \
@@ -526,6 +685,9 @@ dist_TESTS = \
        $(python_tests)
 
 nodist_TESTS =
+if WANT_HTOOLSTESTS
+nodist_TESTS += $(haskell_tests)
+endif
 
 TESTS = $(dist_TESTS) $(nodist_TESTS)
 
@@ -539,14 +701,16 @@ TESTS_ENVIRONMENT = \
 
 all_python_code = \
        $(dist_sbin_SCRIPTS) \
-       $(dist_tools_SCRIPTS) \
+       $(dist_tools_PYTHON) \
        $(pkglib_python_scripts) \
+       $(nodist_pkglib_python_scripts) \
        $(python_tests) \
        $(pkgpython_PYTHON) \
        $(client_PYTHON) \
        $(hypervisor_PYTHON) \
        $(rapi_PYTHON) \
        $(server_PYTHON) \
+       $(pytools_PYTHON) \
        $(http_PYTHON) \
        $(confd_PYTHON) \
        $(masterd_PYTHON) \
@@ -562,19 +726,22 @@ srclink_files = \
        test/daemon-util_unittest.bash \
        test/ganeti-cleaner_unittest.bash \
        test/import-export_unittest.bash \
-       $(all_python_code)
+       $(all_python_code) \
+       $(HS_LIB_SRCS) $(HS_PROG_SRCS)
 
 check_python_code = \
        $(BUILD_BASH_COMPLETION) \
+       $(DOCPP) \
        $(all_python_code)
 
 lint_python_code = \
        ganeti \
        ganeti/http/server.py \
        $(dist_sbin_SCRIPTS) \
-       $(dist_tools_SCRIPTS) \
+       $(dist_tools_PYTHON) \
        $(pkglib_python_scripts) \
        $(BUILD_BASH_COMPLETION) \
+       $(DOCPP) \
        $(PYTHON_BOOTSTRAP)
 
 test/daemon-util_unittest.bash: daemons/daemon-util
@@ -617,7 +784,10 @@ man/footer.html: man/footer.rst
          { echo 'pandoc' not found during configure; exit 1; }
        $(PANDOC) -f rst -t html -o $@ $<
 
-man/%.7.in man/%.8.in: man/%.rst man/footer.man
+man/%.gen: man/%.rst lib/query.py lib/build/sphinx_ext.py
+       PYTHONPATH=. $(RUN_IN_TEMPDIR) $(CURDIR)/$(DOCPP) < $< > $@
+
+man/%.7.in man/%.8.in man/%.1.in: man/%.gen man/footer.man
        @test -n "$(PANDOC)" || \
          { echo 'pandoc' not found during configure; exit 1; }
        set -o pipefail ; \
@@ -625,13 +795,16 @@ man/%.7.in man/%.8.in: man/%.rst man/footer.man
          sed -e 's/\\@/@/g' > $@
        if test -n "$(MAN_HAS_WARNINGS)"; then $(CHECK_MAN) $@; fi
 
-man/%.html.in: man/%.rst man/footer.html
+man/%.html.in: man/%.gen man/footer.html
        @test -n "$(PANDOC)" || \
          { echo 'pandoc' not found during configure; exit 1; }
        set -o pipefail ; \
        $(PANDOC) -s -f rst -t html -A man/footer.html $< | \
          sed -e 's/\\@/@/g' > $@
 
+man/%.1: man/%.1.in $(REPLACE_VARS_SED)
+       sed -f $(REPLACE_VARS_SED) < $< > $@
+
 man/%.7: man/%.7.in $(REPLACE_VARS_SED)
        sed -f $(REPLACE_VARS_SED) < $< > $@
 
@@ -641,8 +814,8 @@ man/%.8: man/%.8.in $(REPLACE_VARS_SED)
 man/%.html: man/%.html.in $(REPLACE_VARS_SED)
        sed -f $(REPLACE_VARS_SED) < $< > $@
 
-epydoc.conf: epydoc.conf.in
-       sed -e 's#@MODULES@#$(lint_python_code)#g' < $< > $@
+epydoc.conf: epydoc.conf.in Makefile
+       sed -e 's#@MODULES@#$(strip $(lint_python_code))#g' < $< > $@
 
 vcs-version:
        if test -d .git; then \
@@ -660,6 +833,16 @@ regen-vcs-version:
          $(MAKE) vcs-version; \
        fi
 
+htools/Ganeti/HTools/Version.hs: htools/Ganeti/HTools/Version.hs.in vcs-version
+       set -e; \
+       VCSVER=`cat $(abs_top_srcdir)/vcs-version`; \
+       sed -e "s/%ver%/$$VCSVER/" < $< > $@
+
+htools/Ganeti/Constants.hs: htools/Ganeti/Constants.hs.in \
+       lib/constants.py lib/_autoconf.py $(CONVERT_CONSTANTS)
+       set -e; \
+       { cat $< ; PYTHONPATH=. $(CONVERT_CONSTANTS); } > $@
+
 lib/_autoconf.py: Makefile vcs-version | lib/.dir
        set -e; \
        VCSVER=`cat $(abs_top_srcdir)/vcs-version`; \
@@ -694,6 +877,8 @@ lib/_autoconf.py: Makefile vcs-version | lib/.dir
          echo "XEN_INITRD = '$(XEN_INITRD)'"; \
          echo "FILE_STORAGE_DIR = '$(FILE_STORAGE_DIR)'"; \
          echo "ENABLE_FILE_STORAGE = $(ENABLE_FILE_STORAGE)"; \
+         echo "SHARED_FILE_STORAGE_DIR = '$(SHARED_FILE_STORAGE_DIR)'"; \
+         echo "ENABLE_SHARED_FILE_STORAGE = $(ENABLE_SHARED_FILE_STORAGE)"; \
          echo "IALLOCATOR_SEARCH_PATH = [$(IALLOCATOR_SEARCH_PATH)]"; \
          echo "KVM_PATH = '$(KVM_PATH)'"; \
          echo "SOCAT_PATH = '$(SOCAT)'"; \
@@ -717,6 +902,11 @@ lib/_autoconf.py: Makefile vcs-version | lib/.dir
          echo "NODED_GROUP = '$(NODED_GROUP)'"; \
          echo "VCS_VERSION = '$$VCSVER'"; \
          echo "DISK_SEPARATOR = '$(DISK_SEPARATOR)'"; \
+         if [ "$(HTOOLS)" ]; then \
+           echo "HTOOLS = True"; \
+         else \
+           echo "HTOOLS = False"; \
+         fi; \
        } > $@
 
 $(REPLACE_VARS_SED): Makefile
@@ -750,6 +940,7 @@ $(REPLACE_VARS_SED): Makefile
 daemons/ganeti-%: MODULE = ganeti.server.$(patsubst ganeti-%,%,$(notdir $@))
 daemons/ganeti-watcher: MODULE = ganeti.watcher
 scripts/%: MODULE = ganeti.client.$(subst -,_,$(notdir $@))
+tools/ensure-dirs: MODULE = ganeti.tools.ensure_dirs
 
 $(PYTHON_BOOTSTRAP): Makefile | $(all_dirfiles)
        test -n "$(MODULE)" || { echo Missing module; exit 1; }
@@ -824,8 +1015,18 @@ check-local: check-dirs
        if test "`head -n 1 $(top_srcdir)/README`" != "Ganeti $$expver"; then \
                echo "Incorrect version in README, expected $$expver"; \
                exit 1; \
+       fi; \
+       if test "`sed -ne '4 p' $(top_srcdir)/doc/iallocator.rst`" != \
+                                       "Documents Ganeti version $$expver"; then \
+               echo "Incorrect version in iallocator.rst, expected $$expver"; \
+               exit 1; \
        fi
 
+.PHONY: hs-check
+hs-check: htools/test
+       @rm -f test.tix
+       ./htools/test
+
 .PHONY: lint
 lint: $(BUILT_SOURCES)
        @test -n "$(PYLINT)" || { echo 'pylint' not found during configure; exit 1; }
@@ -834,6 +1035,11 @@ lint: $(BUILT_SOURCES)
          PYTHONPATH=$(abs_top_srcdir) $(PYLINT) $(LINT_OPTS) \
          --rcfile  ../pylintrc $(patsubst qa/%.py,%,$(qa_scripts))
 
+.PHONY: hlint
+hlint: $(HS_BUILT_SRCS)
+       if tty -s; then C="-c"; else C=""; fi; \
+       hlint --report=doc/hs-lint.html $$C htools
+
 # a dist hook rule for updating the vcs-version file; this is
 # hardcoded due to where it needs to build the file...
 dist-hook:
@@ -893,10 +1099,48 @@ install-exec-local:
        @mkdir_p@ $* && touch $@
 
 .PHONY: apidoc
-apidoc: epydoc.conf $(RUN_IN_TEMPDIR) $(BUILT_SOURCES)
+if WANT_HTOOLSAPIDOC
+apidoc: py-apidoc hs-apidoc
+else
+apidoc: py-apidoc
+endif
+
+.PHONY: py-apidoc
+py-apidoc: epydoc.conf $(RUN_IN_TEMPDIR) $(BUILT_SOURCES)
        $(RUN_IN_TEMPDIR) epydoc -v \
                --conf $(CURDIR)/epydoc.conf \
-               --output $(CURDIR)/doc/api
+               --output $(CURDIR)/$(APIDOC_PY_DIR)
+
+.PHONY: hs-apidoc
+hs-apidoc: $(HS_BUILT_SRCS)
+       @test -n "$(HSCOLOUR)" || \
+           { echo 'HsColour' not found during configure; exit 1; }
+       @test -n "$(HADDOCK)" || \
+           { echo 'haddock' not found during configure; exit 1; }
+       rm -rf $(APIDOC_HS_DIR)/*
+       @mkdir_p@ $(APIDOC_HS_DIR)/Ganeti/HTools/Program
+       $(HSCOLOUR) -print-css > $(APIDOC_HS_DIR)/Ganeti/hscolour.css
+       ln -s ../hscolour.css $(APIDOC_HS_DIR)/Ganeti/HTools/hscolour.css
+       set -e ; \
+       cd htools; \
+       if [ "$(HTOOLS_NOCURL)" ]; \
+       then OPTGHC="--optghc=$(HTOOLS_NOCURL)"; \
+       else OPTGHC=""; \
+       fi; \
+       if [ "$(HTOOLS_PARALLEL3)" ]; \
+       then OPTGHC="$$OPTGHC --optghc=$(HTOOLS_PARALLEL3)"; \
+       fi; \
+       RELSRCS="$(HS_LIB_SRCS:htools/%=%)  $(HS_BUILT_SRCS:htools/%=%)"; \
+       for file in $$RELSRCS; do \
+               hfile=`echo $$file|sed 's/\\.hs$$//'`.html; \
+               $(HSCOLOUR) -css -anchor $$file > ../$(APIDOC_HS_DIR)/$$hfile ; \
+       done ; \
+       $(HADDOCK) --odir ../$(APIDOC_HS_DIR) --html --ignore-all-exports -w \
+               -t ganeti-htools -p haddock-prologue \
+               --source-module="%{MODULE/.//}.html" \
+               --source-entity="%{MODULE/.//}.html#%{NAME}" \
+               $$OPTGHC \
+               $(filter-out Ganeti/HTools/ExtLoader.hs,$(HS_LIB_SRCS:htools/%=%))
 
 .PHONY: TAGS
 TAGS: $(BUILT_SOURCES)
@@ -907,14 +1151,45 @@ TAGS: $(BUILT_SOURCES)
          etags -l python -
 
 .PHONY: coverage
-coverage: $(BUILT_SOURCES) $(python_tests)
+if WANT_HTOOLS
+coverage: py-coverage hs-coverage
+else
+coverage: py-coverage
+endif
+
+.PHONY: py-coverage
+py-coverage: $(BUILT_SOURCES) $(python_tests)
        set -e; \
-       COVERAGE_FILE=$(CURDIR)/doc/coverage/data \
-       TEXT_COVERAGE=$(CURDIR)/doc/coverage/report.txt \
-       HTML_COVERAGE=$(CURDIR)/doc/coverage \
+       COVERAGE_FILE=$(CURDIR)/$(COVERAGE_PY_DIR)/data \
+       TEXT_COVERAGE=$(CURDIR)/$(COVERAGE_PY_DIR)/report.txt \
+       HTML_COVERAGE=$(CURDIR)/$(COVERAGE_PY_DIR) \
        $(PLAIN_TESTS_ENVIRONMENT) $(abs_top_srcdir)/autotools/gen-coverage \
        $(python_tests)
 
+.PHONY: hs-coverage
+hs-coverage: $(haskell_tests)
+       cd htools && rm -f *.tix *.mix && ./test
+       @mkdir_p@ $(COVERAGE_HS_DIR)
+       hpc markup --destdir=$(COVERAGE_HS_DIR) htools/test $(HPCEXCL)
+       hpc report htools/test $(HPCEXCL)
+       ln -sf hpc_index.html $(COVERAGE_HS_DIR)/index.html
+
+# Special "kind-of-QA" target for htools, needs special setup (all
+# tools compiled with -fhpc)
+.PHONY: live-test
+live-test: all
+       set -e ; \
+       cd htools; \
+       rm -f .hpc; ln -s ../.hpc .hpc; \
+       rm -f *.tix *.mix; \
+       ./live-test.sh; \
+       hpc sum --union $(HPCEXCL) $(addsuffix .tix,$(HS_PROGS:htools/%=%)) \
+         --output=live-test.tix ; \
+       @mkdir_p@ ../$(COVERAGE_HS_DIR) ; \
+       hpc markup --destdir=../$(COVERAGE_HS_DIR) live-test \
+               --srcdir=.. $(HPCEXCL) ; \
+       hpc report --srcdir=.. live-test $(HPCEXCL)
+
 commit-check: distcheck lint apidoc
 
 -include ./Makefile.local
diff --git a/NEWS b/NEWS
index 5dd911c..153ff35 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,33 @@
 News
 ====
 
+Version 2.5.0 beta1
+-------------------
+
+*(unreleased)*
+
+- The default of the ``/2/instances/[instance_name]/rename`` RAPI
+  resource's ``ip_check`` parameter changed from ``True`` to ``False``
+  to match the underlying LUXI interface
+- The ``/2/nodes/[node_name]/evacuate`` RAPI resource was changed to use
+  body parameters, see :doc:`RAPI documentation <rapi>`. The server does
+  not maintain backwards-compatibility as the underlying operation
+  changed in an incompatible way. The RAPI client can talk to old
+  servers, but it needs to be told so as the return value changed.
+- When creating file-based instances via RAPI, the ``file_driver``
+  parameter no longer defaults to ``loop`` and must be specified
+- The deprecated "bridge" nic parameter is no longer supported. Use
+  "link" instead.
+- Support for the undocumented and deprecated RAPI instance creation
+  request format version 0 has been dropped. Use version 1, supported
+  since Ganeti 2.1.3 and :doc:`documented <rapi>`, instead.
+- Pyparsing 1.4.6 or above is required, see :doc:`installation
+  documentation <install>`
+- The "cluster-verify" hooks are now executed per group by the
+  OP_CLUSTER_VERIFY_GROUP opcode. This maintains the same behavior if
+  you just run "gnt-cluster verify", which generates one op per group.
+
+
 Version 2.4.3
 -------------
 
diff --git a/UPGRADE b/UPGRADE
index 21217a6..7f29cf1 100644 (file)
--- a/UPGRADE
+++ b/UPGRADE
@@ -257,3 +257,9 @@ Beta 2 switched the config file format to JSON. Steps to upgrade:
 
 The OS definition also need to be upgraded. There is a new version of the
 debian-etch-instance OS (0.2) that goes along with beta 2.
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
diff --git a/autotools/convert-constants b/autotools/convert-constants
new file mode 100755 (executable)
index 0000000..3f2b362
--- /dev/null
@@ -0,0 +1,90 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2011 Google Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+"""Script for converting Python constants to Haskell code fragments.
+
+"""
+
+import re
+
+from ganeti import constants
+
+CONSTANT_RE = re.compile("^[A-Z][A-Z0-9_]+$")
+
+
+def NameRules(name):
+  """Converts the upper-cased Python name to Haskell camelCase.
+
+  """
+  elems = name.split("_")
+  return elems[0].lower() + "".join(e.capitalize() for e in elems[1:])
+
+
+def StringValueRules(value):
+  """Converts a string value from Python to Haskell.
+
+  """
+  value = value.encode("string_escape") # escapes backslashes
+  value = value.replace("\"", "\\\"")
+  return value
+
+
+def Convert():
+  """Converts the constants to Haskell.
+
+  """
+  lines = [""]
+
+  all_names = dir(constants)
+
+  for name in all_names:
+    value = getattr(constants, name)
+    hs_name = NameRules(name)
+    if not CONSTANT_RE.match(name):
+      lines.append("-- Skipped %s, not constant" % name)
+    elif isinstance(value, basestring):
+      lines.append("-- | Converted from Python constant %s" % name)
+      lines.append("%s :: String" % hs_name)
+      lines.append("%s = \"%s\"" % (hs_name, StringValueRules(value)))
+    elif isinstance(value, int):
+      lines.append("-- | Converted from Python constant %s" % name)
+      lines.append("%s :: Int" % hs_name)
+      lines.append("%s = %d" % (hs_name, value))
+    elif isinstance(value, long):
+      lines.append("-- | Converted from Python constant %s" % name)
+      lines.append("%s :: Integer" % hs_name)
+      lines.append("%s = %d" % (hs_name, value))
+    elif isinstance(value, float):
+      lines.append("-- | Converted from Python constant %s" % name)
+      lines.append("%s :: Double" % hs_name)
+      lines.append("%s = %f" % (hs_name, value))
+    else:
+      lines.append("-- Skipped %s, %s not handled" % (name, type(value)))
+    lines.append("")
+
+  return "\n".join(lines)
+
+
+def main():
+  print Convert()
+
+
+if __name__ == "__main__":
+  main()
similarity index 60%
copy from test/ganeti.hypervisor.hv_fake_unittest.py
copy to autotools/docpp
index e0c4240..0970bcb 100755 (executable)
 # 02110-1301, USA.
 
 
-"""Script for testing ganeti.hypervisor.hv_fake"""
+"""Script to replace special directives in documentation.
 
-import unittest
+"""
 
-from ganeti import constants
-from ganeti import objects
-from ganeti import hypervisor
+import re
+import fileinput
 
-from ganeti.hypervisor import hv_fake
+from ganeti import query
+from ganeti.build import sphinx_ext
 
-import testutils
 
+_QUERY_FIELDS_RE = re.compile(r"^@QUERY_FIELDS_(?P<kind>[A-Z]+)@$")
 
-class TestConsole(unittest.TestCase):
-  def test(self):
-    instance = objects.Instance(name="fake.example.com")
-    cons = hv_fake.FakeHypervisor.GetInstanceConsole(instance, {}, {})
-    self.assertTrue(cons.Validate())
-    self.assertEqual(cons.kind, constants.CONS_MESSAGE)
+
+def main():
+  for line in fileinput.input():
+    m = _QUERY_FIELDS_RE.match(line)
+    if m:
+      fields = query.ALL_FIELDS[m.group("kind").lower()]
+      for i in sphinx_ext.BuildQueryFields(fields):
+        print i
+    else:
+      print line,
 
 
 if __name__ == "__main__":
-  testutils.GanetiTestProgram()
+  main()
index 31a696c..e32f863 100755 (executable)
@@ -1,5 +1,8 @@
 #!/bin/bash
 
+# Helper for running things in a temporary directory; used for docs
+# building, unittests, etc.
+
 set -e
 
 tmpdir=$(mktemp -d -t gntbuild.XXXXXXXX)
@@ -7,5 +10,9 @@ trap "rm -rf $tmpdir" EXIT
 
 cp -r autotools daemons scripts lib tools test $tmpdir
 mv $tmpdir/lib $tmpdir/ganeti
+mkdir -p $tmpdir/htools
+if [ -e htools/test ]; then
+  cp -p htools/test $tmpdir/htools/
+fi
 
 cd $tmpdir && GANETI_TEMP_DIR="$tmpdir" "$@"
index 8567729..75d5513 100644 (file)
@@ -111,10 +111,29 @@ AC_ARG_WITH([file-storage-dir],
       enable_file_storage=False
     fi
   ]],
-  [[file_storage_dir="/srv/ganeti/file-storage"; enable_file_storage="True"]])
+  [[file_storage_dir="/srv/ganeti/file-storage";
+    enable_file_storage="True"]])
 AC_SUBST(FILE_STORAGE_DIR, $file_storage_dir)
 AC_SUBST(ENABLE_FILE_STORAGE, $enable_file_storage)
 
+# --with-shared-file-storage-dir=...
+AC_ARG_WITH([shared-file-storage-dir],
+  [AS_HELP_STRING([--with-shared-file-storage-dir=PATH],
+    [directory to store files for shared file-based backend]
+    [ (default is /srv/ganeti/shared-file-storage)]
+  )],
+  [[shared_file_storage_dir="$withval";
+    if test "$withval" != no; then
+      enable_shared_file_storage=True
+    else
+      enable_shared_file_storage=False
+    fi
+  ]],
+  [[shared_file_storage_dir="/srv/ganeti/shared-file-storage";
+    enable_shared_file_storage="True"]])
+AC_SUBST(SHARED_FILE_STORAGE_DIR, $shared_file_storage_dir)
+AC_SUBST(ENABLE_SHARED_FILE_STORAGE, $enable_shared_file_storage)
+
 # --with-kvm-path=...
 AC_ARG_WITH([kvm-path],
   [AS_HELP_STRING([--with-kvm-path=PATH],
@@ -226,10 +245,27 @@ then
 fi
 AC_SUBST(SYSLOG_USAGE, $SYSLOG)
 
+# --enable-htools
+HTOOLS=
+AC_ARG_ENABLE([htools],
+        [AS_HELP_STRING([--enable-htools],
+        [enable use of htools (needs GHC and libraries, default: check)])],
+        [],
+        [enable_htools=check])
+
+# --enable-htools-rapi
+HTOOLS_RAPI=
+AC_ARG_ENABLE([htools-rapi],
+        [AS_HELP_STRING([--enable-htools-rapi],
+        [enable use of RAPI in htools (needs curl, default: no)])],
+        [],
+        [enable_htools_rapi=no])
+
 # --with-disk-separator=...
 AC_ARG_WITH([disk-separator],
   [AS_HELP_STRING([--with-disk-separator=STRING],
-    [Disk index separator, useful if the default of ':' is handled specially by the hypervisor]
+    [Disk index separator, useful if the default of ':' is handled]
+    [ specially by the hypervisor]
   )],
   [disk_separator="$withval"],
   [disk_separator=":"])
@@ -252,7 +288,8 @@ AC_ARG_VAR(SPHINX, [sphinx-build path])
 AC_PATH_PROG(SPHINX, [sphinx-build], [])
 if test -z "$SPHINX"
 then
-  AC_MSG_WARN([sphinx-build not found, documentation rebuild will not be possible])
+  AC_MSG_WARN(m4_normalize([sphinx-build not found, documentation rebuild will
+                            not be possible]))
 fi
 
 # Check for graphviz (dot)
@@ -260,7 +297,8 @@ AC_ARG_VAR(DOT, [dot path])
 AC_PATH_PROG(DOT, [dot], [])
 if test -z "$DOT"
 then
-  AC_MSG_WARN([dot (from the graphviz suite) not found, documentation rebuild not possible])
+  AC_MSG_WARN(m4_normalize([dot (from the graphviz suite) not found,
+                            documentation rebuild not possible]))
 fi
 
 # Check for pylint
@@ -279,6 +317,126 @@ then
   AC_MSG_ERROR([socat not found])
 fi
 
+if test "$enable_htools" != "no"; then
+
+# Check for ghc
+AC_ARG_VAR(GHC, [ghc path])
+AC_PATH_PROG(GHC, [ghc], [])
+if test -z "$GHC"; then
+  if test "$enable_htools" != "check"; then
+    AC_MSG_FAILURE([ghc not found, htools compilation will not possible])
+  fi
+fi
+
+# Check for ghc-pkg
+HTOOLS_MODULES=
+AC_ARG_VAR(GHC_PKG, [ghc-pkg path])
+AC_PATH_PROG(GHC_PKG, [ghc-pkg], [])
+if test -z "$GHC_PKG"; then
+  if test "$enable_htools" != "check"; then
+    AC_MSG_FAILURE([ghc-pkg not found, htools compilation will not be possible])
+  fi
+else
+  # check for modules
+  AC_MSG_NOTICE([checking for required haskell modules])
+  HTOOLS_NOCURL=-DNO_CURL
+  if test "$enable_htools_rapi" != "no"; then
+    AC_MSG_CHECKING([curl])
+    GHC_PKG_CURL=$($GHC_PKG latest curl)
+    if test -z "$GHC_PKG_CURL"; then
+      if test "$enable_htools_rapi" = "check"; then
+        AC_MSG_WARN(m4_normalize([The curl library not found, htools will be
+                                  compiled without RAPI support]))
+      else
+        AC_MSG_FAILURE(m4_normalize([The curl library was not found, but it has
+                                     been requested]))
+      fi
+    else
+      HTOOLS_NOCURL=
+    fi
+    AC_MSG_RESULT($GHC_PKG_CURL)
+  fi
+  AC_SUBST(GHC_PKG_CURL)
+  AC_SUBST(HTOOLS_NOCURL)
+  AC_MSG_CHECKING([parallel])
+  GHC_PKG_PARALLEL=$($GHC_PKG --simple-output list 'parallel-3.*')
+  if test -n "$GHC_PKG_PARALLEL"
+  then
+    HTOOLS_PARALLEL3=-DPARALLEL3
+  else
+    GHC_PKG_PARALLEL=$($GHC_PKG --simple-output list 'parallel-2.*')
+  fi
+  if test -z "$GHC_PKG_PARALLEL"
+  then
+    GHC_PKG_PARALLEL=$($GHC_PKG --simple-output list 'parallel-1.*')
+  fi
+  AC_SUBST(GHC_PKG_PARALLEL)
+  AC_SUBST(HTOOLS_PARALLEL3)
+  AC_MSG_RESULT($GHC_PKG_PARALLEL)
+  AC_MSG_CHECKING([json])
+  GHC_PKG_JSON=$($GHC_PKG latest json)
+  AC_MSG_RESULT($GHC_PKG_JSON)
+  AC_MSG_CHECKING([network])
+  GHC_PKG_NETWORK=$($GHC_PKG latest network)
+  AC_MSG_RESULT($GHC_PKG_NETWORK)
+  AC_MSG_CHECKING([QuickCheck 2.x])
+  GHC_PKG_QUICKCHECK=$($GHC_PKG --simple-output list 'QuickCheck-2.*')
+  AC_MSG_RESULT($GHC_PKG_QUICKCHECK)
+  if test -z "$GHC_PKG_PARALLEL" || test -z "$GHC_PKG_JSON" || \
+     test -z "$GHC_PKG_NETWORK"; then
+    if test "$enable_htools" != "check"; then
+      AC_MSG_FAILURE(m4_normalize([Required Haskell modules not found, htools
+                                   compilation disabled]))
+    fi
+  else
+    # we leave the other modules to be auto-selected
+    HTOOLS_MODULES="-package $GHC_PKG_PARALLEL"
+  fi
+  if test -z "$GHC_PKG_QUICKCHECK"; then
+     AC_MSG_WARN(m4_normalize([The QuickCheck 2.x module was not found,
+                               you won't be able to run Haskell unittests]))
+  fi
+fi
+AC_SUBST(HTOOLS_MODULES)
+AC_SUBST(GHC_PKG_QUICKCHECK)
+
+if test "$enable_htools" != "no"; then
+  if test -z "$GHC" || test -z "$HTOOLS_MODULES"; then
+    AC_MSG_WARN(m4_normalize([Haskell compiler/required libraries not found,
+                              htools compilation disabled]))
+  else
+    HTOOLS=yes
+  fi
+fi
+AC_SUBST(HTOOLS)
+
+# Check for HsColour
+HTOOLS_APIDOC=no
+AC_ARG_VAR(HSCOLOUR, [HsColour path])
+AC_PATH_PROG(HSCOLOUR, [HsColour], [])
+if test -z "$HSCOLOUR"; then
+  AC_MSG_WARN(m4_normalize([HsColour not found, htools API documentation will
+                            not be generated]))
+fi
+
+# Check for haddock
+AC_ARG_VAR(HADDOCK, [haddock path])
+AC_PATH_PROG(HADDOCK, [haddock], [])
+if test -z "$HADDOCK"; then
+  AC_MSG_WARN(m4_normalize([haddock not found, htools API documentation will
+                            not be generated]))
+fi
+if test "$HADDOCK" && test "$HSCOLOUR"; then
+  HTOOLS_APIDOC=yes
+fi
+AC_SUBST(HTOOLS_APIDOC)
+
+fi # end if enable_htools, define automake conditions
+
+AM_CONDITIONAL([WANT_HTOOLS], [test x$HTOOLS = xyes])
+AM_CONDITIONAL([WANT_HTOOLSTESTS], [test x$GHC_PKG_QUICKCHECK != x])
+AM_CONDITIONAL([WANT_HTOOLSAPIDOC], [test x$HTOOLS_APIDOC = xyes])
+
 SOCAT_USE_ESCAPE=
 AC_ARG_ENABLE([socat-escape],
   [AS_HELP_STRING([--enable-socat-escape],
@@ -330,8 +488,8 @@ then
   MAN_HAS_WARNINGS=1
 else
   MAN_HAS_WARNINGS=
-  AC_MSG_WARN([man doesn't support --warnings, man pages checks
-               will not be possible])
+  AC_MSG_WARN(m4_normalize([man does not support --warnings, man page checks
+                            will not be possible]))
 fi
 
 AC_SUBST(MAN_HAS_WARNINGS)
@@ -348,8 +506,9 @@ AC_PYTHON_MODULE(pycurl, t)
 # This is optional but then we've limited functionality
 AC_PYTHON_MODULE(paramiko)
 if test "$HAVE_PYMOD_PARAMIKO" = "no"; then
-  AC_MSG_WARN([You do not have paramiko installed. While this is optional you
-               have to setup SSH and noded on the joining nodes yourself.])
+  AC_MSG_WARN(m4_normalize([You do not have Paramiko installed. While this is
+                            optional you have to configure SSH and the node
+                            daemon on the joining nodes yourself.]))
 fi
 
 AC_CONFIG_FILES([ Makefile ])
diff --git a/daemons/ensure-dirs.in b/daemons/ensure-dirs.in
deleted file mode 100644 (file)
index ff6d744..0000000
+++ /dev/null
@@ -1,168 +0,0 @@
-#!/bin/bash
-
-set -e
-
-LIBDIR="@LOCALSTATEDIR@/lib"
-DATADIR="${LIBDIR}/ganeti"
-RUNDIR="@LOCALSTATEDIR@/run"
-GNTRUNDIR="${RUNDIR}/ganeti"
-LOGDIR="@LOCALSTATEDIR@/log"
-GNTLOGDIR="${LOGDIR}/ganeti"
-LOCKDIR="@LOCALSTATEDIR@/lock"
-
-_fileset_owner() {
-  case "$1" in
-    masterd)
-      echo "@GNTMASTERUSER@:@GNTMASTERDGROUP@"
-      ;;
-    confd)
-      echo "@GNTCONFDUSER@:@GNTCONFDGROUP@"
-      ;;
-    rapi)
-      echo "@GNTRAPIUSER@:@GNTRAPIGROUP@"
-      ;;
-    noded)
-      echo "root:@GNTMASTERDGROUP@"
-      ;;
-    daemons)
-      echo "@GNTMASTERUSER@:@GNTDAEMONSGROUP@"
-      ;;
-    masterd-confd)
-      echo "@GNTMASTERUSER@:@GNTCONFDGROUP@"
-      ;;
-    *)
-      echo "root:root"
-      ;;
-  esac
-}
-
-_ensure_file() {
-  local file="$1"
-  local perm="$2"
-  local owner="$3"
-
-  [[ -e "${file}" ]] || return 1
-  chmod ${perm} "${file}"
-
-  if ! [[ -z "${owner}" ]]; then
-    chown ${owner} "${file}"
-  fi
-
-  return 0
-}
-
-_ensure_dir() {
-  local dir="$1"
-  local perm="$2"
-  local owner="$3"
-
-  [[ -d "${dir}" ]] || mkdir "${dir}"
-
-  _ensure_file "${dir}" "${perm}" "${owner}"
-}
-
-_gather_files() {
-  local path="$1"
-  local perm="$2"
-  local user="$3"
-  local group="$4"
-
-  shift 4
-
-  find "${path}" -type f "(" "!" -perm ${perm} -or "(" "!" -user ${user} -or \
-       "!" -group ${group} ")" ")" "$@"
-}
-
-_ensure_datadir() {
-  local full_run="$1"
-
-  _ensure_dir ${DATADIR} 0755 "$(_fileset_owner masterd)"
-  _ensure_dir ${DATADIR}/queue 0700 "$(_fileset_owner masterd)"
-  _ensure_dir ${DATADIR}/queue/archive 0700 "$(_fileset_owner masterd)"
-  _ensure_dir ${DATADIR}/uidpool 0750 "$(_fileset_owner noded)"
-  _ensure_dir ${DATADIR}/rapi 0750 "$(_fileset_owner rapi)"
-
-  # We ignore these files if they don't exists (incomplete setup)
-  _ensure_file ${DATADIR}/cluster-domain-secret 0640 \
-               "$(_fileset_owner masterd)" || :
-  _ensure_file ${DATADIR}/config.data 0640 "$(_fileset_owner masterd-confd)" || :
-  _ensure_file ${DATADIR}/hmac.key 0440 "$(_fileset_owner confd)" || :
-  _ensure_file ${DATADIR}/known_hosts 0644 "$(_fileset_owner masterd)" || :
-  _ensure_file ${DATADIR}/rapi.pem 0440 "$(_fileset_owner rapi)" || :
-  _ensure_file ${DATADIR}/rapi/users 0640 "$(_fileset_owner rapi)" || :
-  _ensure_file ${DATADIR}/server.pem 0440 "$(_fileset_owner masterd)" || :
-  _ensure_file ${DATADIR}/queue/serial 0600 "$(_fileset_owner masterd)" || :
-
-  # To not change the utils.LockFile object
-  touch ${DATADIR}/queue/lock
-  _ensure_file ${DATADIR}/queue/lock 0600 "$(_fileset_owner masterd)"
-
-  if ! [[ -z "${full_run}" ]]; then
-    local queue_owner="$(_fileset_owner masterd)"
-    local ssconf_owner="$(_fileset_owner noded)"
-
-    _gather_files ${DATADIR}/queue 0600 @GNTMASTERUSER@ @GNTMASTERDGROUP@ | \
-    while read path; do
-      _ensure_file "$path" 0600 "$queue_owner"
-    done
-
-    _gather_files ${DATADIR} 0600 root @GNTMASTERDGROUP@ -name 'ssconf_*' | \
-    while read path; do
-      _ensure_file "$path" 0444 "$ssconf_owner"
-    done
-  fi
-}
-
-_ensure_rundir() {
-  _ensure_dir ${GNTRUNDIR} 0775 "$(_fileset_owner daemons)"
-  _ensure_dir ${GNTRUNDIR}/socket 0750 "$(_fileset_owner daemons)"
-  _ensure_dir ${GNTRUNDIR}/bdev-cache 0755 "$(_fileset_owner noded)"
-  _ensure_dir ${GNTRUNDIR}/instance-disks 0755 "$(_fileset_owner noded)"
-  _ensure_dir ${GNTRUNDIR}/crypto 0700 "$(_fileset_owner noded)"
-  _ensure_dir ${GNTRUNDIR}/import-export 0755 "$(_fileset_owner noded)"
-
-  # We ignore this file if it don't exists (not yet start up)
-  _ensure_file ${GNTRUNDIR}/socket/ganeti-master 0770 \
-               "$(_fileset_owner daemons)" || :
-}
-
-_ensure_logdir() {
-  _ensure_dir ${GNTLOGDIR} 0770 "$(_fileset_owner daemons)"
-  _ensure_dir ${GNTLOGDIR}/os 0750 "$(_fileset_owner daemons)"
-
-  # We ignore these files if they don't exists (incomplete setup)
-  _ensure_file ${GNTLOGDIR}/master-daemon.log 0600 "$(_fileset_owner masterd)" || :
-  _ensure_file ${GNTLOGDIR}/conf-daemon.log 0600 "$(_fileset_owner confd)" || :
-  _ensure_file ${GNTLOGDIR}/node-daemon.log 0600 "$(_fileset_owner noded)" || :
-  _ensure_file ${GNTLOGDIR}/rapi-daemon.log 0600 "$(_fileset_owner rapi)" || :
-}
-
-_ensure_lockdir() {
-  _ensure_dir ${LOCKDIR} 1777 ""
-}
-
-_operate_while_hold() {
-  local fn=$1
-  local path=$2
-  shift 2
-
-  (cd ${path};
-   ${fn} "$@")
-}
-
-main() {
-  local full_run
-
-  while getopts "f" OPTION; do
-    case ${OPTION} in
-      f) full_run=1 ;;
-    esac
-  done
-
-  _operate_while_hold "_ensure_datadir" ${DATADIR} ${full_run}
-  _operate_while_hold "_ensure_rundir" ${RUNDIR}
-  _operate_while_hold "_ensure_logdir" ${LOGDIR}
-  _operate_while_hold "_ensure_lockdir" @LOCALSTATEDIR@
-}
-
-main "$@"
index 9626ffe..192b881 100644 (file)
@@ -1540,10 +1540,10 @@ See :doc:`separate documentation for move-instance <move-instance>`.
 Other Ganeti projects
 ---------------------
 
-There are two other Ganeti-related projects that can be useful in a
-Ganeti deployment. These can be downloaded from the project site
-(http://code.google.com/p/ganeti/) and the repositories are also on the
-project git site (http://git.ganeti.org).
+Below is a list (which might not be up-to-date) of additional projects
+that can be useful in a Ganeti deployment. They can be downloaded from
+the project site (http://code.google.com/p/ganeti/) and the repositories
+are also on the project git site (http://git.ganeti.org).
 
 NBMA tools
 ++++++++++
@@ -1557,14 +1557,11 @@ archive.
 ganeti-htools
 +++++++++++++
 
-The ``ganeti-htools`` software consists of a set of tools:
-
-- ``hail``: an advanced iallocator script compared to Ganeti's builtin
-  one
-- ``hbal``: a tool for rebalancing the cluster, i.e. moving instances
-  around in order to better use the resources on the nodes
-- ``hspace``: a tool for estimating the available capacity of a cluster,
-  so that capacity planning can be done efficiently
+Before Ganeti version 2.5, this was a standalone project; since that
+version it is integrated into the Ganeti codebase (see
+:doc:`install-quick` for instructions on how to enable it). If you run
+an older Ganeti version, you will have to download and build it
+separately.
 
 For more information and installation instructions, see the README file
 in the source archive.
index 25d9c0d..e15902f 100644 (file)
@@ -22,7 +22,7 @@ import sys, os
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.todo']
+extensions = ['sphinx.ext.todo', "ganeti.build.sphinx_ext"]
 
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
index 123afa2..75d1859 100644 (file)
@@ -187,6 +187,8 @@ format and decode them on the receiver side.
 For more details about the RAPI daemon see `Remote API changes`_, and
 for the node daemon see `Node daemon changes`_.
 
+.. _luxi:
+
 The LUXI protocol
 +++++++++++++++++
 
@@ -606,6 +608,8 @@ be leaf locks or carefully structured non-leaf ones, to avoid deadlock
 race conditions.
 
 
+.. _jqueue-original-design:
+
 Job Queue
 ~~~~~~~~~
 
index f04ab8e..bdf4755 100644 (file)
@@ -568,6 +568,8 @@ removed. Ganeti itself will allow clearing of both flags, even though
 this doesn't make much sense currently.
 
 
+.. _jqueue-job-priority-design:
+
 Job priorities
 --------------
 
diff --git a/doc/design-chained-jobs.rst b/doc/design-chained-jobs.rst
new file mode 100644 (file)
index 0000000..4061d96
--- /dev/null
@@ -0,0 +1,237 @@
+============
+Chained jobs
+============
+
+.. contents:: :depth: 4
+
+This is a design document about the innards of Ganeti's job processing.
+Readers are advised to study previous design documents on the topic:
+
+- :ref:`Original job queue <jqueue-original-design>`
+- :ref:`Job priorities <jqueue-job-priority-design>`
+- :doc:`LU-generated jobs <design-lu-generated-jobs>`
+
+
+Current state and shortcomings
+==============================
+
+Ever since the introduction of the job queue with Ganeti 2.0 there have
+been situations where we wanted to run several jobs in a specific order.
+Due to the job queue's current design, such a guarantee can not be
+given. Jobs are run according to their priority, their ability to
+acquire all necessary locks and other factors.
+
+One way to work around this limitation is to do some kind of job
+grouping in the client code. Once all jobs of a group have finished, the
+next group is submitted and waited for. There are different kinds of
+clients for Ganeti, some of which don't share code (e.g. Python clients
+vs. htools). This design proposes a solution which would be implemented
+as part of the job queue in the master daemon.
+
+
+Proposed changes
+================
+
+With the implementation of :ref:`job priorities
+<jqueue-job-priority-design>` the processing code was re-architectured
+and became a lot more versatile. It now returns jobs to the queue in
+case the locks for an opcode can't be acquired, allowing other
+jobs/opcodes to be run in the meantime.
+
+The proposal is to add a new, optional property to opcodes to define
+dependencies on other jobs. Job X could define opcodes with a dependency
+on the success of job Y and would only be run once job Y is finished. If
+there's a dependency on success and job Y failed, job X would fail as
+well. Since such dependencies would use job IDs, the jobs still need to
+be submitted in the right order.
+
+.. pyassert::
+
+   # Update description below if finalized job status change
+   constants.JOBS_FINALIZED == frozenset([
+     constants.JOB_STATUS_CANCELED,
+     constants.JOB_STATUS_SUCCESS,
+     constants.JOB_STATUS_ERROR,
+     ])
+
+The new attribute's value would be a list of two-valued tuples. Each
+tuple contains a job ID and a list of requested status for the job
+depended upon. Only final status are accepted
+(:pyeval:`utils.CommaJoin(constants.JOBS_FINALIZED)`). An empty list is
+equivalent to specifying all final status (except
+:pyeval:`constants.JOB_STATUS_CANCELED`, which is treated specially).
+An opcode runs only once all its dependency requirements have been
+fulfilled.
+
+Any job referring to a cancelled job is also cancelled unless it
+explicitely lists :pyeval:`constants.JOB_STATUS_CANCELED` as a requested
+status.
+
+In case a referenced job can not be found in the normal queue or the
+archive, referring jobs fail as the status of the referenced job can't
+be determined.
+
+With this change, clients can submit all wanted jobs in the right order
+and proceed to wait for changes on all these jobs (see
+``cli.JobExecutor``). The master daemon will take care of executing them
+in the right order, while still presenting the client with a simple
+interface.
+
+Clients using the ``SubmitManyJobs`` interface can use relative job IDs
+(negative integers) to refer to jobs in the same submission.
+
+.. highlight:: javascript
+
+Example data structures::
+
+  # First job
+  {
+    "job_id": "6151",
+    "ops": [
+      { "OP_ID": "OP_INSTANCE_REPLACE_DISKS", ..., },
+      { "OP_ID": "OP_INSTANCE_FAILOVER", ..., },
+      ],
+  }
+
+  # Second job, runs in parallel with first job
+  {
+    "job_id": "7687",
+    "ops": [
+      { "OP_ID": "OP_INSTANCE_MIGRATE", ..., },
+      ],
+  }
+
+  # Third job, depending on success of previous jobs
+  {
+    "job_id": "9218",
+    "ops": [
+      { "OP_ID": "OP_NODE_SET_PARAMS",
+        "depend": [
+          [6151, ["success"]],
+          [7687, ["success"]],
+          ],
+        "offline": True, },
+      ],
+  }
+
+
+Implementation details
+----------------------
+
+Status while waiting for dependencies
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Jobs waiting for dependencies are certainly not in the queue anymore and
+therefore need to change their status from "queued". While waiting for
+opcode locks the job is in the "waiting" status (the constant is named
+``JOB_STATUS_WAITLOCK``, but the actual value is ``waiting``). There the
+following possibilities:
+
+#. Introduce a new status, e.g. "waitdeps".
+
+   Pro:
+
+   - Clients know for sure a job is waiting for dependencies, not locks
+
+   Con:
+
+   - Code and tests would have to be updated/extended for the new status
+   - List of possible state transitions certainly wouldn't get simpler
+   - Breaks backwards compatibility, older clients might get confused
+
+#. Use existing "waiting" status.
+
+   Pro:
+
+   - No client changes necessary, less code churn (note that there are
+     clients which don't live in Ganeti core)
+   - Clients don't need to know the difference between waiting for a job
+     and waiting for a lock; it doesn't make a difference
+   - Fewer state transitions (see commit ``5fd6b69479c0``, which removed
+     many state transitions and disk writes)
+
+   Con:
+
+   - Not immediately visible what a job is waiting for, but it's the
+     same issue with locks; this is the reason why the lock monitor
+     (``gnt-debug locks``) was introduced; job dependencies can be shown
+     as "locks" in the monitor
+
+Based on these arguments, the proposal is to do the following:
+
+- Rename ``JOB_STATUS_WAITLOCK`` constant to ``JOB_STATUS_WAITING`` to
+  reflect its actual meanting: the job is waiting for something
+- While waiting for dependencies and locks, jobs are in the "waiting"
+  status
+- Export dependency information in lock monitor; example output::
+
+    Name      Mode Owner Pending
+    job/27491 -    -     success:job/34709,job/21459
+    job/21459 -    -     success,error:job/14513
+
+
+Cost of deserialization
+~~~~~~~~~~~~~~~~~~~~~~~
+
+To determine the status of a dependency job the job queue must have
+access to its data structure. Other queue operations already do this,
+e.g. archiving, watching a job's progress and querying jobs.
+
+Initially (Ganeti 2.0/2.1) the job queue shared the job objects
+in memory and protected them using locks. Ganeti 2.2 (see :doc:`design
+document <design-2.2>`) changed the queue to read and deserialize jobs
+from disk. This significantly reduced locking and code complexity.
+Nowadays inotify is used to wait for changes on job files when watching
+a job's progress.
+
+Reading from disk and deserializing certainly has some cost associated
+with it, but it's a significantly simpler architecture than
+synchronizing in memory with locks. At the stage where dependencies are
+evaluated the queue lock is held in shared mode, so different workers
+can read at the same time (deliberately ignoring CPython's interpreter
+lock).
+
+It is expected that the majority of executed jobs won't use
+dependencies and therefore won't be affected.
+
+
+Other discussed solutions
+=========================
+
+Job-level attribute
+-------------------
+
+At a first look it might seem to be better to put dependencies on
+previous jobs at a job level. However, it turns out that having the
+option of defining only a single opcode in a job as having such a
+dependency can be useful as well. The code complexity in the job queue
+is equivalent if not simpler.
+
+Since opcodes are guaranteed to run in order, clients can just define
+the dependency on the first opcode.
+
+Another reason for the choice of an opcode-level attribute is that the
+current LUXI interface for submitting jobs is a bit restricted and would
+need to be changed to allow the addition of job-level attributes,
+potentially requiring changes in all LUXI clients and/or breaking
+backwards compatibility.
+
+
+Client-side logic
+-----------------
+
+There's at least one implementation of a batched job executor twisted
+into the ``burnin`` tool's code. While certainly possible, a client-side
+solution should be avoided due to the different clients already in use.
+For one, the :doc:`remote API <rapi>` client shouldn't import
+non-standard modules. htools are written in Haskell and can't use Python
+modules. A batched job executor contains quite some logic. Even if
+cleanly abstracted in a (Python) library, sharing code between different
+clients is difficult if not impossible.
+
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
diff --git a/doc/design-cpu-pinning.rst b/doc/design-cpu-pinning.rst
new file mode 100644 (file)
index 0000000..f1b3de1
--- /dev/null
@@ -0,0 +1,225 @@
+Ganeti CPU Pinning
+==================
+
+Objective
+---------
+
+This document defines Ganeti's support for CPU pinning (aka CPU
+affinity).
+
+CPU pinning enables mapping and unmapping entire virtual machines or a
+specific virtual CPU (vCPU), to a physical CPU or a range of CPUs.
+
+At this stage Pinning will be implemented for Xen and KVM.
+
+Command Line
+------------
+
+Suggested command line parameters for controlling CPU pinning are as
+follows::
+
+  gnt-instance modify -H cpu_mask=<cpu-pinning-info> <instance>
+
+cpu-pinning-info can be any of the following:
+
+* One vCPU mapping, which can be the word "all" or a combination
+  of CPU numbers and ranges separated by comma. In this case, all
+  vCPUs will be mapped to the indicated list.
+* A list of vCPU mappings, separated by a colon ':'. In this case
+  each vCPU is mapped to an entry in the list, and the size of the
+  list must match the number of vCPUs defined for the instance. This
+  is enforced when setting CPU pinning or when setting the number of
+  vCPUs using ``-B vcpus=#``.
+
+  The mapping list is matched to consecutive virtual CPUs, so the first entry
+  would be the CPU pinning information for vCPU 0, the second entry
+  for vCPU 1, etc.
+
+The default setting for new instances is "all", which maps the entire
+instance to all CPUs, thus effectively turning off CPU pinning.
+
+Here are some usage examples::
+
+  # Map vCPU 0 to physical CPU 1 and vCPU 1 to CPU 3 (assuming 2 vCPUs)
+  gnt-instance modify -H cpu_mask=1:3 my-inst
+
+  # Pin vCPU 0 to CPUs 1 or 2, and vCPU 1 to any CPU
+  gnt-instance modify -H cpu_mask=1-2:all my-inst
+
+  # Pin vCPU 0 to any CPU, vCPU 1 to CPUs 1, 3, 4 or 5, and CPU 2 to
+  # CPU 0
+  gnt-instance modify -H cpu_mask=all:1\\,3-4:0 my-inst
+
+  # Pin entire VM to CPU 0
+  gnt-instance modify -H cpu_mask=0 my-inst
+
+  # Turn off CPU pinning (default setting)
+  gnt-instance modify -H cpu_mask=all my-inst
+
+Assuming an instance has 2 vCPUs, the following commands will fail::
+
+  # not enough mappings
+  gnt-instance modify -H cpu_mask=0 my-inst
+
+  # too many
+  gnt-instance modify -H cpu_mask=2:1:1 my-inst
+
+Validation
+----------
+
+CPU pinning information is validated by making sure it matches the
+number of vCPUs. This validation happens when changing either the
+cpu_mask or vcpus parameters.
+Changing either parameter in a way that conflicts with the other will
+fail with a proper error message.
+To make such a change, both parameters should be modified at the same
+time. For example:
+``gnt-instance modify -B vcpus=4 -H cpu_mask=1:1:2-3:4\\,6 my-inst``
+
+Besides validating CPU configuration, i.e. the number of vCPUs matches
+the requested CPU pinning, Ganeti will also verify the number of
+physical CPUs is enough to support the required configuration. For
+example, trying to run a configuration of vcpus=2,cpu_mask=0:4 on
+a node with 4 cores will fail (Note: CPU numbers are 0-based).
+
+This validation should repeat every time an instance is started or
+migrated live. See more details under Migration below.
+
+Cluster verification should also test the compatibility of other nodes in
+the cluster to required configuration and alert if a minimum requirement
+is not met.
+
+Failover
+--------
+
+CPU pinning configuration can be transferred from node to node, unless
+the number of physical CPUs is smaller than what the configuration calls
+for.  It is suggested that unless this is the case, all transfers and
+migrations will succeed.
+
+In case the number of physical CPUs is smaller than the numbers
+indicated by CPU pinning information, instance failover will fail.
+
+In case of emergency, to force failover to ignore mismatching CPU
+information, the following switch can be used:
+``gnt-instance failover --ignore-cpu-mismatch my-inst``.
+This command will try to fail the instance with the current cpu mask,
+but if that fails, it will change the mask to be "all".
+
+Migration
+---------
+
+In case of live migration, and in addition to failover considerations,
+it is required to remap CPU pinning after migration. This can be done in
+realtime for instances for both Xen and KVM, and only depends on the
+number of physical CPUs being sufficient to support the migrated
+instance.
+
+Data
+----
+
+Pinning information will be kept as a list of integers per vCPU.
+To mark a mapping of any CPU, we will use (-1).
+A single entry, no matter what the number of vCPUs is, will always mean
+that all vCPUs have the same mapping.
+
+Configuration file
+------------------
+
+The pinning information is kept for each instance's hypervisor
+params section of the configuration file as
+``cpu_mask: [ [ a ], [ b, c ], [ d ] ]``
+
+Xen
+---
+
+There are 2 ways to control pinning in Xen, either via the command line
+or through the configuration file.
+
+The commands to make direct pinning changes are the following::
+
+  # To pin a vCPU to a specific CPU
+  xm vcpu-pin <domain> <vcpu> <cpu>
+
+  # To unpin a vCPU
+  xm vcpu-pin <domain> <vcpu> all
+
+  # To get the current pinning status
+  xm vcpu-list <domain>
+
+Since currently controlling Xen in Ganeti is done in the configuration
+file, it is straight forward to use the same method for CPU pinning.
+There are 2 different parameters that control Xen's CPU pinning and
+configuration:
+
+vcpus
+  controls the number of vCPUs
+cpus
+  maps vCPUs to physical CPUs
+
+When no pinning is required (pinning information is "all"), the
+"cpus" entry is removed from the configuration file.
+
+For all other cases, the configuration is "translated" to Xen, which
+expects either ``cpus = "a"`` or ``cpus = [ "a", "b", "c", ...]``,
+where each a, b or c are a physical CPU number, CPU range, or a
+combination, and the number of entries (if a list is used) must match
+the number of vCPUs, and are mapped in order.
+
+For example, CPU pinning information of ``1:2,4-7:0-1`` is translated
+to this entry in Xen's configuration ``cpus = [ "1", "2,4-7", "0-1" ]``
+
+KVM
+---
+
+Controlling pinning in KVM is a little more complicated as there is no
+configuration to control pinning before instances are started.
+
+The way to change or assign CPU pinning under KVM is to use ``taskset`` or
+its underlying system call ``sched_setaffinity``. Setting the affinity for
+the VM process will change CPU pinning for the entire VM, and setting it
+for specific vCPU threads will control specific vCPUs.
+
+The sequence of commands to control pinning is this: start the instance
+with the ``-S`` switch, so it halts before starting execution, get the
+process ID or identify thread IDs of each vCPU by sending ``info cpus``
+to the monitor, map vCPUs as required by the cpu-pinning information,
+and issue a ``cont`` command on the KVM monitor to allow the instance
+to start execution.
+
+For example, a sequence of commands to control CPU affinity under KVM
+may be:
+
+* Start KVM: ``/usr/bin/kvm … <kvm-command-line-options> … -S``
+* Use socat to connect to monitor
+* send ``info cpus`` to monitor to get thread/vCPU information
+* call ``sched_setaffinity`` for each thread with the CPU mask
+* send ``cont`` to KVM's monitor
+
+A CPU mask is a hexadecimal bit mask where each bit represents one
+physical CPU. See man page for :manpage:`sched_setaffinity(2)` for more
+details.
+
+For example, to run a specific thread-id on CPUs 1 or 3 the mask is
+0x0000000A.
+
+We will control process and thread affinity using the python affinity
+package (http://pypi.python.org/pypi/affinity). This package is a Python
+wrapper around the two affinity system calls, and has no other
+requirements.
+
+Alternative Design Options
+--------------------------
+
+1. There's an option to ignore the limitations of the underlying
+   hypervisor and instead of requiring explicit pinning information
+   for *all* vCPUs, assume a mapping of "all" to vCPUs not mentioned.
+   This can lead to inadvertent missing information, but either way,
+   since using cpu-pinning options is probably not going to be
+   frequent, there's no real advantage.
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
diff --git a/doc/design-draft.rst b/doc/design-draft.rst
new file mode 100644 (file)
index 0000000..79468d0
--- /dev/null
@@ -0,0 +1,21 @@
+======================
+Design document drafts
+======================
+
+.. toctree::
+   :maxdepth: 2
+
+   design-x509-ca.rst
+   design-http-server.rst
+   design-impexp2.rst
+   design-lu-generated-jobs.rst
+   design-multi-reloc.rst
+   design-cpu-pinning.rst
+   design-chained-jobs.rst
+   design-ovf-support.rst
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
diff --git a/doc/design-htools-2.3.rst b/doc/design-htools-2.3.rst
new file mode 100644 (file)
index 0000000..527963f
--- /dev/null
@@ -0,0 +1,327 @@
+====================================
+ Synchronising htools to Ganeti 2.3
+====================================
+
+Ganeti 2.3 introduces a number of new features that change the cluster
+internals significantly enough that the htools suite needs to be
+updated accordingly in order to function correctly.
+
+Shared storage support
+======================
+
+Currently, the htools algorithms presume a model where all of an
+instance's resources is served from within the cluster, more
+specifically from the nodes comprising the cluster. While is this
+usual for memory and CPU, deployments which use shared storage will
+invalidate this assumption for storage.
+
+To account for this, we need to move some assumptions from being
+implicit (and hardcoded) to being explicitly exported from Ganeti.
+
+
+New instance parameters
+-----------------------
+
+It is presumed that Ganeti will export for all instances a new
+``storage_type`` parameter, that will denote either internal storage
+(e.g. *plain* or *drbd*), or external storage.
+
+Furthermore, a new ``storage_pool`` parameter will classify, for both
+internal and external storage, the pool out of which the storage is
+allocated. For internal storage, this will be either ``lvm`` (the pool
+that provides space to both ``plain`` and ``drbd`` instances) or
+``file`` (for file-storage-based instances). For external storage,
+this will be the respective NAS/SAN/cloud storage that backs up the
+instance. Note that for htools, external storage pools are opaque; we
+only care that they have an identifier, so that we can distinguish
+between two different pools.
+
+If these two parameters are not present, the instances will be
+presumed to be ``internal/lvm``.
+
+New node parameters
+-------------------
+
+For each node, it is expected that Ganeti will export what storage
+types it supports and pools it has access to. So a classic 2.2 cluster
+will have all nodes supporting ``internal/lvm`` and/or
+``internal/file``, whereas a new shared storage only 2.3 cluster could
+have ``external/my-nas`` storage.
+
+Whatever the mechanism that Ganeti will use internally to configure
+the associations between nodes and storage pools, we consider that
+we'll have available two node attributes inside htools: the list of internal
+and external storage pools.
+
+External storage and instances
+------------------------------
+
+Currently, for an instance we allow one cheap move type: failover to
+the current secondary, if it is a healthy node, and four other
+“expensive” (as in, including data copies) moves that involve changing
+either the secondary or the primary node or both.
+
+In presence of an external storage type, the following things will
+change:
+
+- the disk-based moves will be disallowed; this is already a feature
+  in the algorithm, controlled by a boolean switch, so adapting
+  external storage here will be trivial
+- instead of the current one secondary node, the secondaries will
+  become a list of potential secondaries, based on access to the
+  instance's storage pool
+
+Except for this, the basic move algorithm remains unchanged.
+
+External storage and nodes
+--------------------------
+
+Two separate areas will have to change for nodes and external storage.
+
+First, then allocating instances (either as part of a move or a new
+allocation), if the instance is using external storage, then the
+internal disk metrics should be ignored (for both the primary and
+secondary cases).
+
+Second, the per-node metrics used in the cluster scoring must take
+into account that nodes might not have internal storage at all, and
+handle this as a well-balanced case (score 0).
+
+N+1 status
+----------
+
+Currently, computing the N+1 status of a node is simple:
+
+- group the current secondary instances by their primary node, and
+  compute the sum of each instance group memory
+- choose the maximum sum, and check if it's smaller than the current
+  available memory on this node
+
+In effect, computing the N+1 status is a per-node matter. However,
+with shared storage, we don't have secondary nodes, just potential
+secondaries. Thus computing the N+1 status will be a cluster-level
+matter, and much more expensive.
+
+A simple version of the N+1 checks would be that for each instance
+having said node as primary, we have enough memory in the cluster for
+relocation. This means we would actually need to run allocation
+checks, and update the cluster status from within allocation on one
+node, while being careful that we don't recursively check N+1 status
+during this relocation, which is too expensive.
+
+However, the shared storage model has some properties that changes the
+rules of the computation. Speaking broadly (and ignoring hard
+restrictions like tag based exclusion and CPU limits), the exact
+location of an instance in the cluster doesn't matter as long as
+memory is available. This results in two changes:
+
+- simply tracking the amount of free memory buckets is enough,
+  cluster-wide
+- moving an instance from one node to another would not change the N+1
+  status of any node, and only allocation needs to deal with N+1
+  checks
+
+Unfortunately, this very cheap solution fails in case of any other
+exclusion or prevention factors.
+
+TODO: find a solution for N+1 checks.
+
+
+Node groups support
+===================
+
+The addition of node groups has a small impact on the actual
+algorithms, which will simply operate at node group level instead of
+cluster level, but it requires the addition of new algorithms for
+inter-node group operations.
+
+The following two definitions will be used in the following
+paragraphs:
+
+local group
+  The local group refers to a node's own node group, or when speaking
+  about an instance, the node group of its primary node
+
+regular cluster
+  A cluster composed of a single node group, or pre-2.3 cluster
+
+super cluster
+  This term refers to a cluster which comprises multiple node groups,
+  as opposed to a 2.2 and earlier cluster with a single node group
+
+In all the below operations, it's assumed that Ganeti can gather the
+entire super cluster state cheaply.
+
+
+Balancing changes
+-----------------
+
+Balancing will move from cluster-level balancing to group
+balancing. In order to achieve a reasonable improvement in a super
+cluster, without needing to keep state of what groups have been
+already balanced previously, the balancing algorithm will run as
+follows:
+
+#. the cluster data is gathered
+#. if this is a regular cluster, as opposed to a super cluster,
+   balancing will proceed normally as previously
+#. otherwise, compute the cluster scores for all groups
+#. choose the group with the worst score and see if we can improve it;
+   if not choose the next-worst group, so on
+#. once a group has been identified, run the balancing for it
+
+Of course, explicit selection of a group will be allowed.
+
+Super cluster operations
+++++++++++++++++++++++++
+
+Beside the regular group balancing, in a super cluster we have more
+operations.
+
+
+Redistribution
+^^^^^^^^^^^^^^
+
+In a regular cluster, once we run out of resources (offline nodes
+which can't be fully evacuated, N+1 failures, etc.) there is nothing
+we can do unless nodes are added or instances are removed.
+
+In a super cluster however, there might be resources available in
+another group, so there is the possibility of relocating instances
+between groups to re-establish N+1 success within each group.
+
+One difficulty in the presence of both super clusters and shared
+storage is that the move paths of instances are quite complicated;
+basically an instance can move inside its local group, and to any
+other groups which have access to the same storage type and storage
+pool pair. In effect, the super cluster is composed of multiple
+‘partitions’, each containing one or more groups, but a node is
+simultaneously present in multiple partitions, one for each storage
+type and storage pool it supports. As such, the interactions between
+the individual partitions are too complex for non-trivial clusters to
+assume we can compute a perfect solution: we might need to move some
+instances using shared storage pool ‘A’ in order to clear some more
+memory to accept an instance using local storage, which will further
+clear more VCPUs in a third partition, etc. As such, we'll limit
+ourselves at simple relocation steps within a single partition.
+
+Algorithm:
+
+#. read super cluster data, and exit if cluster doesn't allow
+   inter-group moves
+#. filter out any groups that are “alone” in their partition
+   (i.e. no other group sharing at least one storage method)
+#. determine list of healthy versus unhealthy groups:
+
+    #. a group which contains offline nodes still hosting instances is
+       definitely not healthy
+    #. a group which has nodes failing N+1 is ‘weakly’ unhealthy
+
+#. if either list is empty, exit (no work to do, or no way to fix problems)
+#. for each unhealthy group:
+
+    #. compute the instances that are causing the problems: all
+       instances living on offline nodes, all instances living as
+       secondary on N+1 failing nodes, all instances living as primaries
+       on N+1 failing nodes (in this order)
+    #. remove instances, one by one, until the source group is healthy
+       again
+    #. try to run a standard allocation procedure for each instance on
+       all potential groups in its partition
+    #. if all instances were relocated successfully, it means we have a
+       solution for repairing the original group
+
+Compression
+^^^^^^^^^^^
+
+In a super cluster which has had many instance reclamations, it is
+possible that while none of the groups is empty, overall there is
+enough empty capacity that an entire group could be removed.
+
+The algorithm for “compressing” the super cluster is as follows:
+
+#. read super cluster data
+#. compute total *(memory, disk, cpu)*, and free *(memory, disk, cpu)*
+   for the super-cluster
+#. computer per-group used and free *(memory, disk, cpu)*
+#. select candidate groups for evacuation:
+
+    #. they must be connected to other groups via a common storage type
+       and pool
+    #. they must have fewer used resources than the global free
+       resources (minus their own free resources)
+
+#. for each of these groups, try to relocate all its instances to
+   connected peer groups
+#. report the list of groups that could be evacuated, or if instructed
+   so, perform the evacuation of the group with the largest free
+   resources (i.e. in order to reclaim the most capacity)
+
+Load balancing
+^^^^^^^^^^^^^^
+
+Assuming a super cluster using shared storage, where instance failover
+is cheap, it should be possible to do a load-based balancing across
+groups.
+
+As opposed to the normal balancing, where we want to balance on all
+node attributes, here we should look only at the load attributes; in
+other words, compare the available (total) node capacity with the
+(total) load generated by instances in a given group, and computing
+such scores for all groups, trying to see if we have any outliers.
+
+Once a reliable load-weighting method for groups exists, we can apply
+a modified version of the cluster scoring method to score not
+imbalances across nodes, but imbalances across groups which result in
+a super cluster load-related score.
+
+Allocation changes
+------------------
+
+It is important to keep the allocation method across groups internal
+(in the Ganeti/Iallocator combination), instead of delegating it to an
+external party (e.g. a RAPI client). For this, the IAllocator protocol
+should be extended to provide proper group support.
+
+For htools, the new algorithm will work as follows:
+
+#. read/receive cluster data from Ganeti
+#. filter out any groups that do not supports the requested storage
+   method
+#. for remaining groups, try allocation and compute scores after
+   allocation
+#. sort valid allocation solutions accordingly and return the entire
+   list to Ganeti
+
+The rationale for returning the entire group list, and not only the
+best choice, is that we anyway have the list, and Ganeti might have
+other criteria (e.g. the best group might be busy/locked down, etc.)
+so even if from the point of view of resources it is the best choice,
+it might not be the overall best one.
+
+Node evacuation changes
+-----------------------
+
+While the basic concept in the ``multi-evac`` iallocator
+mode remains unchanged (it's a simple local group issue), when failing
+to evacuate and running in a super cluster, we could have resources
+available elsewhere in the cluster for evacuation.
+
+The algorithm for computing this will be the same as the one for super
+cluster compression and redistribution, except that the list of
+instances is fixed to the ones living on the nodes to-be-evacuated.
+
+If the inter-group relocation is successful, the result to Ganeti will
+not be a local group evacuation target, but instead (for each
+instance) a pair *(remote group, nodes)*. Ganeti itself will have to
+decide (based on user input) whether to continue with inter-group
+evacuation or not.
+
+In case that Ganeti doesn't provide complete cluster data, just the
+local group, the inter-group relocation won't be attempted.
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
diff --git a/doc/design-http-server.rst b/doc/design-http-server.rst
new file mode 100644 (file)
index 0000000..06553a7
--- /dev/null
@@ -0,0 +1,154 @@
+=========================================
+Design for replacing Ganeti's HTTP server
+=========================================
+
+.. contents:: :depth: 4
+
+.. _http-srv-shortcomings:
+
+Current state and shortcomings
+------------------------------
+
+The :doc:`new design for import/export <design-impexp2>` depends on an
+HTTP server. Ganeti includes a home-grown HTTP server based on Python's
+``BaseHTTPServer``. While it served us well so far, it only implements
+the very basics of the HTTP protocol. It is, for example, not structured
+well enough to support chunked transfers (:rfc:`2616`, section 3.6.1),
+which would have some advantages. In addition, it has not been designed
+for sending large responses.
+
+In the case of the node daemon the HTTP server can not easily be
+separated from the actual backend code and therefore must run as "root".
+The RAPI daemon does request parsing in the same process as talking to
+the master daemon via LUXI.
+
+
+Proposed changes
+----------------
+
+The proposal is to start using a full-fledged HTTP server in Ganeti and
+to run Ganeti's code as `FastCGI <http://www.fastcgi.com/>`_
+applications. Reasons:
+
+- Simplify Ganeti's code by delegating the details of HTTP and SSL to
+  another piece of software
+- Run HTTP frontend and handler backend as separate processes and users
+  (esp. useful for node daemon, but also import/export and Remote API)
+- Allows implementation of :ref:`rpc-feedback`
+
+
+Software choice
++++++++++++++++
+
+Theoretically any server able of speaking FastCGI to a backend process
+could be used. However, to keep the number of steps required for setting
+up a new cluster at roughly the same level, the implementation will be
+geared for one specific HTTP server at the beginning. Support for other
+HTTP servers can still be implemented.
+
+After a rough selection of available HTTP servers `lighttpd
+<http://www.lighttpd.net/>`_ and `nginx <http://www.nginx.org/>`_ were
+the most likely candidates. Both are `widely used`_ and tested.
+
+.. _widely used: http://news.netcraft.com/archives/2011/01/12/
+  january-2011-web-server-survey-4.html
+
+Nginx' `original documentation <http://sysoev.ru/nginx/docs/>`_ is in
+Russian, translations are `available in a Wiki
+<http://wiki.nginx.org/>`_. Nginx does not support old-style CGI
+programs.
+
+The author found `lighttpd's documentation
+<http://redmine.lighttpd.net/wiki/lighttpd>`_ easier to understand and
+was able to configure a test server quickly. This, together with the
+support for more technologies, made deciding easier.
+
+With its use as a public-facing web server on a large number of websites
+(and possibly more behind proxies), lighttpd should be a safe choice.
+Unlike other webservers, such as the Apache HTTP Server, lighttpd's
+codebase is of manageable size.
+
+Initially the HTTP server would only be used for import/export
+transfers, but its use can be expanded to the Remote API and node
+daemon (see :ref:`rpc-feedback`).
+
+To reduce the attack surface, an option will be provided to configure
+services (e.g. import/export) to only listen on certain network
+interfaces.
+
+
+.. _rpc-feedback:
+
+RPC feedback
+++++++++++++
+
+HTTP/1.1 supports chunked transfers (:rfc:`2616`, section 3.6.1). They
+could be used to provide feedback from node daemons to the master,
+similar to the feedback from jobs. A good use would be to provide
+feedback to the user during long-running operations, e.g. downloading an
+instance's data from another cluster.
+
+.. _requirement: http://www.python.org/dev/peps/pep-0333/
+  #buffering-and-streaming
+
+WSGI 1.0 (:pep:`333`) includes the following `requirement`_:
+
+  WSGI servers, gateways, and middleware **must not** delay the
+  transmission of any block; they **must** either fully transmit the
+  block to the client, or guarantee that they will continue transmission
+  even while the application is producing its next block
+
+This behaviour was confirmed to work with lighttpd and the
+:ref:`flup <http-software-req>` library. FastCGI by itself has no such
+guarantee; webservers with buffering might require artificial padding to
+force the message to be transmitted.
+
+The node daemon can send JSON-encoded messages back to the master daemon
+by separating them using a predefined character (see :ref:`LUXI
+<luxi>`). The final message contains the method's result. pycURL passes
+each received chunk to the callback set as ``CURLOPT_WRITEFUNCTION``.
+Once a message is complete, the master daemon can pass it to a callback
+function inside the job, which then decides on what to do (e.g. forward
+it as job feedback to the user).
+
+A more detailed design may have to be written before deciding whether to
+implement RPC feedback.
+
+
+.. _http-software-req:
+
+Software requirements
++++++++++++++++++++++
+
+- lighttpd 1.4.24 or above built with OpenSSL support (earlier versions
+  `don't support SSL client certificates
+  <http://redmine.lighttpd.net/issues/1288>`_)
+- `flup <http://trac.saddi.com/flup>`_ for FastCGI
+
+
+Lighttpd SSL configuration
+++++++++++++++++++++++++++
+
+.. highlight:: lighttpd
+
+The following sample shows how to configure SSL with client certificates
+in Lighttpd::
+
+  $SERVER["socket"] == ":443" {
+    ssl.engine = "enable"
+    ssl.pemfile = "server.pem"
+    ssl.ca-file = "ca.pem"
+    ssl.use-sslv2  = "disable"
+    ssl.cipher-list = "HIGH:-DES:-3DES:-EXPORT:-ADH"
+    ssl.verifyclient.activate = "enable"
+    ssl.verifyclient.enforce = "enable"
+    ssl.verifyclient.exportcert = "enable"
+    ssl.verifyclient.username = "SSL_CLIENT_S_DN_CN"
+  }
+
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
diff --git a/doc/design-impexp2.rst b/doc/design-impexp2.rst
new file mode 100644 (file)
index 0000000..5b996fe
--- /dev/null
@@ -0,0 +1,559 @@
+==================================
+Design for import/export version 2
+==================================
+
+.. contents:: :depth: 4
+
+Current state and shortcomings
+------------------------------
+
+Ganeti 2.2 introduced :doc:`inter-cluster instance moves <design-2.2>`
+and replaced the import/export mechanism with the same technology. It's
+since shown that the chosen implementation was too complicated and and
+can be difficult to debug.
+
+The old implementation is henceforth called "version 1". It used
+``socat`` in combination with a rather complex tree of ``bash`` and
+Python utilities to move instances between clusters and import/export
+them inside the cluster. Due to protocol limitations, the master daemon
+starts a daemon on the involved nodes and then keeps polling a status
+file for updates. A non-trivial number of timeouts ensures that jobs
+don't freeze.
+
+In version 1, the destination node would start a daemon listening on a
+random TCP port. Upon receiving the destination information, the source
+node would temporarily stop the instance, create snapshots, and start
+exporting the data by connecting to the destination. The random TCP port
+is chosen by the operating system by binding the socket to port 0.
+While this is a somewhat elegant solution, it causes problems in setups
+with restricted connectivity (e.g. iptables).
+
+Another issue encountered was with dual-stack IPv6 setups. ``socat`` can
+only listen on one protocol, IPv4 or IPv6, at a time. The connecting
+node can not simply resolve the DNS name, but it must be told the exact
+IP address.
+
+Instance OS definitions can provide custom import/export scripts. They
+were working well in the early days when a filesystem was usually
+created directly on the block device. Around Ganeti 2.0 there was a
+transition to using partitions on the block devices. Import/export
+scripts could no longer use simple ``dump`` and ``restore`` commands,
+but usually ended up doing raw data dumps.
+
+
+Proposed changes
+----------------
+
+Unlike in version 1, in version 2 the destination node will connect to
+the source. The active side is swapped. This design assumes the
+following design documents have been implemented:
+
+- :doc:`design-x509-ca`
+- :doc:`design-http-server`
+
+The following design is mostly targetted at inter-cluster instance
+moves. Intra-cluster import and export use the same technology, but do
+so in a less complicated way (e.g. reusing the node daemon certificate
+in version 1).
+
+Support for instance OS import/export scripts, which have been in Ganeti
+since the beginning, will be dropped with this design. Should the need
+arise, they can be re-added later.
+
+
+Software requirements
++++++++++++++++++++++
+
+- HTTP client: cURL/pycURL (already used for inter-node RPC and RAPI
+  client)
+- Authentication: X509 certificates (server and client)
+
+
+Transport
++++++++++
+
+Instead of a home-grown, mostly raw protocol the widely used HTTP
+protocol will be used. Ganeti already uses HTTP for its :doc:`Remote API
+<rapi>` and inter-node communication. Encryption and authentication will
+be implemented using SSL and X509 certificates.
+
+
+SSL certificates
+++++++++++++++++
+
+The source machine will identify connecting clients by their SSL
+certificate. Unknown certificates will be refused.
+
+Version 1 created a new self-signed certificate per instance
+import/export, allowing the certificate to be used as a Certificate
+Authority (CA). This worked by means of starting a new ``socat``
+instance per instance import/export.
+
+Under the version 2 model, a continously running HTTP server will be
+used. This disallows the use of self-signed certificates for
+authentication as the CA needs to be the same for all issued
+certificates.
+
+See the :doc:`separate design document for more details on how the
+certificate authority will be implemented <design-x509-ca>`.
+
+Local imports/exports will, like version 1, use the node daemon's
+certificate/key. Doing so allows the verification of local connections.
+The client's certificate can be exported to the CGI/FastCGI handler
+using lighttpd's ``ssl.verifyclient.exportcert`` setting. If a
+cluster-local import/export is being done, the handler verifies if the
+used certificate matches with the local node daemon key.
+
+
+Source
+++++++
+
+The source can be the same physical machine as the destination, another
+node in the same cluster, or a node in another cluster. A
+physical-to-virtual migration mechanism could be implemented as an
+alternative source.
+
+In the case of a traditional import, the source is usually a file on the
+source machine. For exports and remote imports, the source is an
+instance's raw disk data. In all cases the transported data is opaque to
+Ganeti.
+
+All nodes of a cluster will run an instance of Lighttpd. The
+configuration is automatically generated when starting Ganeti. The HTTP
+server is configured to listen on IPv4 and IPv6 simultaneously.
+Imports/exports will use a dedicated TCP port, similar to the Remote
+API.
+
+See the separate :ref:`HTTP server design document
+<http-srv-shortcomings>` for why Ganeti's existing, built-in HTTP server
+is not a good choice.
+
+The source cluster is provided with a X509 Certificate Signing Request
+(CSR) for a key private to the destination cluster.
+
+After shutting down the instance, creating snapshots and restarting the
+instance the master will sign the destination's X509 certificate using
+the :doc:`X509 CA <design-x509-ca>` once per instance disk. Instead of
+using another identifier, the certificate's serial number (:ref:`never
+reused <x509-ca-serial>`) and fingerprint are used to identify incoming
+requests. Once ready, the master will call an RPC method on the source
+node and provide it with the input information (e.g. file paths or block
+devices) and the certificate identities.
+
+The RPC method will write the identities to a place accessible by the
+HTTP request handler, generate unique transfer IDs and return them to
+the master. The transfer ID could be a filename containing the
+certificate's serial number, fingerprint and some disk information. The
+file containing the per-transfer information is signed using the node
+daemon key and the signature written to a separate file.
+
+Once everything is in place, the master sends the certificates, the data
+and notification URLs (which include the transfer IDs) and the public
+part of the source's CA to the job submitter. Like in version 1,
+everything will be signed using the cluster domain secret.
+
+Upon receiving a request, the handler verifies the identity and
+continues to stream the instance data. The serial number and fingerprint
+contained in the transfer ID should be matched with the certificate
+used. If a cluster-local import/export was requested, the remote's
+certificate is verified with the local node daemon key. The signature of
+the information file from which the handler takes the path of the block
+device (and more) is verified using the local node daemon certificate.
+There are two options for handling requests, :ref:`CGI
+<lighttpd-cgi-opt>` and :ref:`FastCGI <lighttpd-fastcgi-opt>`.
+
+To wait for all requests to finish, the master calls another RPC method.
+The destination should notify the source once it's done with downloading
+the data. Since this notification may never arrive (e.g. network
+issues), an additional timeout needs to be used.
+
+There is no good way to avoid polling as the HTTP requests will be
+handled asynchronously in another process. Once, and if, implemented
+:ref:`RPC feedback <rpc-feedback>` could be used to combine the two RPC
+methods.
+
+Upon completion of the transfer requests, the instance is removed if
+requested.
+
+
+.. _lighttpd-cgi-opt:
+
+Option 1: CGI
+~~~~~~~~~~~~~
+
+While easier to implement, this option requires the HTTP server to
+either run as "root" or a so-called SUID binary to elevate the started
+process to run as "root".
+
+The export data can be sent directly to the HTTP server without any
+further processing.
+
+
+.. _lighttpd-fastcgi-opt:
+
+Option 2: FastCGI
+~~~~~~~~~~~~~~~~~
+
+Unlike plain CGI, FastCGI scripts are run separately from the webserver.
+The webserver talks to them via a Unix socket. Webserver and scripts can
+run as separate users. Unlike for CGI, there are almost no bootstrap
+costs attached to each request.
+
+The FastCGI protocol requires data to be sent in length-prefixed
+packets, something which wouldn't be very efficient to do in Python for
+large amounts of data (instance imports/exports can be hundreds of
+gigabytes). For this reason the proposal is to use a wrapper program
+written in C (e.g. `fcgiwrap
+<http://nginx.localdomain.pl/wiki/FcgiWrap>`_) and to write the handler
+like an old-style CGI program with standard input/output. If data should
+be copied from a file, ``cat``, ``dd`` or ``socat`` can be used (see
+note about :ref:`sendfile(2)/splice(2) with Python <python-sendfile>`).
+
+The bootstrap cost associated with starting a Python interpreter for
+a disk export is expected to be negligible.
+
+The `spawn-fcgi <http://cgit.stbuehler.de/gitosis/spawn-fcgi/about/>`_
+program will be used to start the CGI wrapper as "root".
+
+FastCGI is, in the author's opinion, the better choice as it allows user
+separation. As a first implementation step the export handler can be run
+as a standard CGI program. User separation can be implemented as a
+second step.
+
+
+Destination
++++++++++++
+
+The destination can be the same physical machine as the source, another
+node in the same cluster, or a node in another cluster. While not
+considered in this design document, instances could be exported from the
+cluster by implementing an external client for exports.
+
+For traditional exports the destination is usually a file on the
+destination machine. For imports and remote exports, the destination is
+an instance's disks. All transported data is opaque to Ganeti.
+
+Before an import can be started, an RSA key and corresponding
+Certificate Signing Request (CSR) must be generated using the new opcode
+``OpInstanceImportPrepare``. The returned information is signed using
+the cluster domain secret. The RSA key backing the CSR must not leave
+the destination cluster. After being passed through a third party, the
+source cluster will generate signed certificates from the CSR.
+
+Once the request for creating the instance arrives at the master daemon,
+it'll create the instance and call an RPC method on the instance's
+primary node to download all data. The RPC method does not return until
+the transfer is complete or failed (see :ref:`EXP_SIZE_FD <exp-size-fd>`
+and :ref:`RPC feedback <rpc-feedback>`).
+
+The node will use pycURL to connect to the source machine and identify
+itself with the signed certificate received. pycURL will be configured
+to write directly to a file descriptor pointing to either a regular file
+or block device. The file descriptor needs to point to the correct
+offset for resuming downloads.
+
+Using cURL's multi interface, more than one transfer can be made at the
+same time. While parallel transfers are used by the version 1
+import/export, it can be decided at a later time whether to use them in
+version 2 too. More investigation is necessary to determine whether
+``CURLOPT_MAXCONNECTS`` is enough to limit the number of connections or
+whether more logic is necessary.
+
+If a transfer fails before it's finished (e.g. timeout or network
+issues) it should be retried using an exponential backoff delay. The
+opcode submitter can specify for how long the transfer should be
+retried.
+
+At the end of a transfer, succssful or not, the source cluster must be
+notified. A the same time the RSA key needs to be destroyed.
+
+Support for HTTP proxies can be implemented by setting
+``CURLOPT_PROXY``. Proxies could be used for moving instances in/out of
+restricted network environments or across protocol borders (e.g. IPv4
+networks unable to talk to IPv6 networks).
+
+
+The big picture for instance moves
+----------------------------------
+
+#. ``OpInstanceImportPrepare`` (destination cluster)
+
+  Create RSA key and CSR (certificate signing request), return signed
+  with cluster domain secret.
+
+#. ``OpBackupPrepare`` (source cluster)
+
+  Becomes a no-op in version 2, but see :ref:`backwards-compat`.
+
+#. ``OpBackupExport`` (source cluster)
+
+  - Receives destination cluster's CSR, verifies signature using
+    cluster domain secret.
+  - Creates certificates using CSR and :doc:`cluster CA
+    <design-x509-ca>`, one for each disk
+  - Stop instance, create snapshots, start instance
+  - Prepare HTTP resources on node
+  - Send certificates, URLs and CA certificate to job submitter using
+    feedback mechanism
+  - Wait for all transfers to finish or fail (with timeout)
+  - Remove snapshots
+
+#. ``OpInstanceCreate`` (destination cluster)
+
+  - Receives certificates signed by destination cluster, verifies
+    certificates and URLs using cluster domain secret
+
+    Note that the parameters should be implemented in a generic way
+    allowing future extensions, e.g. to download disk images from a
+    public, remote server. The cluster domain secret allows Ganeti to
+    check data received from a third party, but since this won't work
+    with such extensions, other checks will have to be designed.
+
+  - Create block devices
+  - Download every disk from source, verified using remote's CA and
+    authenticated using signed certificates
+  - Destroy RSA key and certificates
+  - Start instance
+
+.. TODO: separate create from import?
+
+
+.. _impexp2-http-resources:
+
+HTTP resources on source
+------------------------
+
+The HTTP resources listed below will be made available by the source
+machine. The transfer ID is generated while preparing the export and is
+unique per disk and instance. No caching should be used and the
+``Pragma`` (HTTP/1.0) and ``Cache-Control`` (HTTP/1.1) headers set
+accordingly by the server.
+
+``GET /transfers/[transfer_id]/contents``
+  Dump disk contents. Important request headers:
+
+  ``Accept`` (:rfc:`2616`, section 14.1)
+    Specify preferred media types. Only one type is supported in the
+    initial implementation:
+
+    ``application/octet-stream``
+      Request raw disk content.
+
+    If support for more media types were to be implemented in the
+    future, the "q" parameter used for "indicating a relative quality
+    factor" needs to be used. In the meantime parameters need to be
+    expected, but can be ignored.
+
+    If support for OS scripts were to be re-added in the future, the
+    MIME type ``application/x-ganeti-instance-export`` is hereby
+    reserved for disk dumps using an export script.
+
+    If the source can not satisfy the request the response status code
+    will be 406 (Not Acceptable). Successful requests will specify the
+    used media type using the ``Content-Type`` header. Unless only
+    exactly one media type is requested, the client must handle the
+    different response types.
+
+  ``Accept-Encoding`` (:rfc:`2616`, section 14.3)
+    Specify desired content coding. Supported are ``identity`` for
+    uncompressed data, ``gzip`` for compressed data and ``*`` for any.
+    The response will include a ``Content-Encoding`` header with the
+    actual coding used. If the client specifies an unknown coding, the
+    response status code will be 406 (Not Acceptable).
+
+    If the client specifically needs compressed data (see
+    :ref:`impexp2-compression`) but only gets ``identity``, it can
+    either compress locally or abort the request.
+
+  ``Range`` (:rfc:`2616`, section 14.35)
+    Raw disk dumps can be resumed using this header (e.g. after a
+    network issue).
+
+    If this header was given in the request and the source supports
+    resuming, the status code of the response will be 206 (Partial
+    Content) and it'll include the ``Content-Range`` header as per
+    :rfc:`2616`. If it does not support resuming or the request was not
+    specifying a range, the status code will be 200 (OK).
+
+    Only a single byte range is supported. cURL does not support
+    ``multipart/byteranges`` responses by itself. Even if they could be
+    somehow implemented, doing so would be of doubtful benefit for
+    import/export.
+
+    For raw data dumps handling ranges is pretty straightforward by just
+    dumping the requested range.
+
+    cURL will fail with the error code ``CURLE_RANGE_ERROR`` if a
+    request included a range but the server can't handle it. The request
+    must be retried without a range.
+
+``POST /transfers/[transfer_id]/done``
+  Use this resource to notify the source when transfer is finished (even
+  if not successful). The status code will be 204 (No Content).
+
+
+Code samples
+------------
+
+pycURL to file
+++++++++++++++
+
+.. highlight:: python
+
+The following code sample shows how to write downloaded data directly to
+a file without pumping it through Python::
+
+  curl = pycurl.Curl()
+  curl.setopt(pycurl.URL, "http://www.google.com/")
+  curl.setopt(pycurl.WRITEDATA, open("googlecom.html", "w"))
+  curl.perform()
+
+This works equally well if the file descriptor is a pipe to another
+process.
+
+
+.. _backwards-compat:
+
+Backwards compatibility
+-----------------------
+
+.. _backwards-compat-v1:
+
+Version 1
++++++++++
+
+The old inter-cluster import/export implementation described in the
+:doc:`Ganeti 2.2 design document <design-2.2>` will be supported for at
+least one minor (2.x) release. Intra-cluster imports/exports will use
+the new version right away.
+
+
+.. _exp-size-fd:
+
+``EXP_SIZE_FD``
++++++++++++++++
+
+Together with the improved import/export infrastructure Ganeti 2.2
+allowed instance export scripts to report the expected data size. This
+was then used to provide the user with an estimated remaining time.
+Version 2 no longer supports OS import/export scripts and therefore
+``EXP_SIZE_FD`` is no longer needed.
+
+
+.. _impexp2-compression:
+
+Compression
++++++++++++
+
+Version 1 used explicit compression using ``gzip`` for transporting
+data, but the dumped files didn't use any compression. Version 2 will
+allow the destination to specify which encoding should be used. This way
+the transported data is already compressed and can be directly used by
+the client (see :ref:`impexp2-http-resources`). The cURL option
+``CURLOPT_ENCODING`` can be used to set the ``Accept-Encoding`` header.
+cURL will not decompress received data when
+``CURLOPT_HTTP_CONTENT_DECODING`` is set to zero (if another HTTP client
+library were used which doesn't support disabling transparent
+compression, a custom content-coding type could be defined, e.g.
+``x-ganeti-gzip``).
+
+
+Notes
+-----
+
+The HTTP/1.1 protocol (:rfc:`2616`) defines trailing headers for chunked
+transfers in section 3.6.1. This could be used to transfer a checksum at
+the end of an import/export. cURL supports trailing headers since
+version 7.14.1. Lighttpd doesn't seem to support them for FastCGI, but
+they appear to be usable in combination with an NPH CGI (No Parsed
+Headers).
+
+.. _lighttp-sendfile:
+
+Lighttpd allows FastCGI applications to send the special headers
+``X-Sendfile`` and ``X-Sendfile2`` (the latter with a range). Using
+these headers applications can send response headers and tell the
+webserver to serve regular file stored on the file system as a response
+body. The webserver will then take care of sending that file.
+Unfortunately this mechanism is restricted to regular files and can not
+be used for data from programs, neither direct nor via named pipes,
+without writing to a file first. The latter is not an option as instance
+data can be very large. Theoretically ``X-Sendfile`` could be used for
+sending the input for a file-based instance import, but that'd require
+the webserver to run as "root".
+
+.. _python-sendfile:
+
+Python does not include interfaces for the ``sendfile(2)`` or
+``splice(2)`` system calls. The latter can be useful for faster copying
+of data between file descriptors. There are some 3rd-party modules (e.g.
+http://pypi.python.org/pypi/py-sendfile/) and discussions
+(http://bugs.python.org/issue10882) for including support for
+``sendfile(2)``, but the later is certainly not going to happen for the
+Python versions supported by Ganeti. Calling the function using the
+``ctypes`` module might be possible.
+
+
+Performance considerations
+--------------------------
+
+The design described above was confirmed to be one of the better choices
+in terms of download performance with bigger block sizes. All numbers
+were gathered on the same physical machine with a single CPU and 1 GB of
+RAM while downloading 2 GB of zeros read from ``/dev/zero``. ``wget``
+(version 1.10.2) was used as the client, ``lighttpd`` (version 1.4.28)
+as the server. The numbers in the first line are in megabytes per
+second. The second line in each row is the CPU time spent in userland
+respective system (measured for the CGI/FastCGI program using ``time
+-v``).
+
+::
+
+  ----------------------------------------------------------------------
+  Block size                      4 KB    64 KB   128 KB    1 MB    4 MB
+  ======================================================================
+  Plain CGI script reading          83      174      180     122     120
+  from ``/dev/zero``
+                               0.6/3.9  0.1/2.4  0.1/2.2 0.0/1.9 0.0/2.1
+  ----------------------------------------------------------------------
+  FastCGI with ``fcgiwrap``,        86      167      170     177     174
+  ``dd`` reading from
+  ``/dev/zero``                  1.1/5  0.5/2.9  0.5/2.7 0.7/3.1 0.7/2.8
+  ----------------------------------------------------------------------
+  FastCGI with ``fcgiwrap``,        68      146      150     170     170
+  Python script copying from
+  ``/dev/zero`` to stdout
+                               1.3/5.1  0.8/3.7  0.7/3.3  0.9/2.9  0.8/3
+  ----------------------------------------------------------------------
+  FastCGI, Python script using      31       48       47       5       1
+  ``flup`` library (version
+  1.0.2) reading from
+  ``/dev/zero``
+                              23.5/9.8 14.3/8.5   16.1/8       -       -
+  ----------------------------------------------------------------------
+
+
+It should be mentioned that the ``flup`` library is not implemented in
+the most efficient way, but even with some changes it doesn't get much
+faster. It is fine for small amounts of data, but not for huge
+transfers.
+
+
+Other considered solutions
+--------------------------
+
+Another possible solution considered was to use ``socat`` like version 1
+did. Due to the changing model, a large part of the code would've
+required a rewrite anyway, while still not fixing all shortcomings. For
+example, ``socat`` could still listen on only one protocol, IPv4 or
+IPv6. Running two separate instances might have fixed that, but it'd get
+more complicated. Using an existing HTTP server will provide us with a
+number of other benefits as well, such as easier user separation between
+server and backend.
+
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
diff --git a/doc/design-lu-generated-jobs.rst b/doc/design-lu-generated-jobs.rst
new file mode 100644 (file)
index 0000000..5f76c95
--- /dev/null
@@ -0,0 +1,94 @@
+==================================
+Submitting jobs from logical units
+==================================
+
+.. contents:: :depth: 4
+
+This is a design document about the innards of Ganeti's job processing.
+Readers are advised to study previous design documents on the topic:
+
+- :ref:`Original job queue <jqueue-original-design>`
+- :ref:`Job priorities <jqueue-job-priority-design>`
+
+
+Current state and shortcomings
+==============================
+
+Some Ganeti operations want to execute as many operations in parallel as
+possible. Examples are evacuating or failing over a node (``gnt-node
+evacuate``/``gnt-node failover``). Without changing large parts of the
+code, e.g. the RPC layer, to be asynchronous, or using threads inside a
+logical unit, only a single operation can be executed at a time per job.
+
+Currently clients work around this limitation by retrieving the list of
+desired targets and then re-submitting a number of jobs. This requires
+logic to be kept in the client, in some cases leading to duplication
+(e.g. CLI and RAPI).
+
+
+Proposed changes
+================
+
+The job queue lock is guaranteed to be released while executing an
+opcode/logical unit. This means an opcode can talk to the job queue and
+submit more jobs. It then receives the job IDs, like any job submitter
+using the LUXI interface would. These job IDs are returned to the
+client, who then will then proceed to wait for the jobs to finish.
+
+Technically, the job queue already passes a number of callbacks to the
+opcode processor. These are used for giving user feedback, notifying the
+job queue of an opcode having gotten its locks, and checking whether the
+opcode has been cancelled. A new callback function is added to submit
+jobs. Its signature and result will be equivalent to the job queue's
+existing ``SubmitManyJobs`` function.
+
+Logical units can submit jobs by returning an instance of a special
+container class with a list of jobs, each of which is a list of opcodes
+(e.g.  ``[[op1, op2], [op3]]``). The opcode processor will recognize
+instances of the special class when used a return value and will submit
+the contained jobs. The submission status and job IDs returned by the
+submission callback are used as the opcode's result. It should be
+encapsulated in a dictionary allowing for future extensions.
+
+.. highlight:: javascript
+
+Example::
+
+  {
+    "jobs": [
+      (True, "8149"),
+      (True, "21019"),
+      (False, "Submission failed"),
+      (True, "31594"),
+      ],
+  }
+
+Job submissions can fail for variety of reasons, e.g. a full or drained
+job queue. Lists of jobs can not be submitted atomically, meaning some
+might fail while others succeed. The client is responsible for handling
+such cases.
+
+
+Other discussed solutions
+=========================
+
+Instead of requiring the client to wait for the returned jobs, another
+idea was to do so from within the submitting opcode in the master
+daemon. While technically possible, doing so would have two major
+drawbacks:
+
+- Opcodes waiting for other jobs to finish block one job queue worker
+  thread
+- All locks must be released before starting the waiting process,
+  failure to do so can lead to deadlocks
+
+Instead of returning the job IDs as part of the normal opcode result,
+introducing a new opcode field, e.g. ``op_jobids``, was discussed and
+dismissed. A new field would touch many areas and possibly break some
+assumptions. There were also questions about the semantics.
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
diff --git a/doc/design-multi-reloc.rst b/doc/design-multi-reloc.rst
new file mode 100644 (file)
index 0000000..039d51d
--- /dev/null
@@ -0,0 +1,149 @@
+====================================
+Moving instances accross node groups
+====================================
+
+This design document explains the changes needed in Ganeti to perform
+instance moves across node groups. Reader familiarity with the following
+existing documents is advised:
+
+- :doc:`Current IAllocator specification <iallocator>`
+- :doc:`Shared storage model in 2.3+ <design-shared-storage>`
+
+Motivation and and design proposal
+==================================
+
+At the moment, moving instances away from their primary or secondary
+nodes with the ``relocate`` and ``multi-evacuate`` IAllocator calls
+restricts target nodes to those on the same node group. This ensures a
+mobility domain is never crossed, and allows normal operation of each
+node group to be confined within itself.
+
+It is desirable, however, to have a way of moving instances across node
+groups so that, for example, it is possible to move a set of instances
+to another group for policy reasons, or completely empty a given group
+to perform maintenance operations.
+
+To implement this, we propose the addition of new IAllocator calls to
+compute inter-group instance moves and group-aware node evacuation,
+taking into account mobility domains as appropriate. The interface
+proposed below should be enough to cover the use cases mentioned above.
+
+With the implementation of this design proposal, the previous
+``multi-evacuate`` mode will be deprecated.
+
+.. _multi-reloc-detailed-design:
+
+Detailed design
+===============
+
+All requests honor the groups' ``alloc_policy`` attribute.
+
+Changing instance's groups
+--------------------------
+
+Takes a list of instances and a list of node group UUIDs; the instances
+will be moved away from their current group, to any of the groups in the
+target list. All instances need to have their primary node in the same
+group, which may not be a target group. If the target group list is
+empty, the request is simply "change group" and the instances are placed
+in any group but their original one.
+
+Node evacuation
+---------------
+
+Evacuates instances off their primary nodes. The evacuation mode
+can be given as ``primary-only``, ``secondary-only`` or
+``all``. The call is given a list of instances whose primary nodes need
+to be in the same node group. The returned nodes need to be in the same
+group as the original primary node.
+
+.. _multi-reloc-result:
+
+Result
+------
+
+In all storage models, an inter-group move can be modeled as a sequence
+of **replace secondary**, **migration** and **failover** operations
+(when shared storage is used, they will all be failover or migration
+operations within the corresponding mobility domain).
+
+The result of the operations described above must contain two lists of
+instances and a list of jobs (each of which is a list of serialized
+opcodes) to actually execute the operation. :doc:`Job dependencies
+<design-chained-jobs>` can be used to force jobs to run in a certain
+order while still making use of parallelism.
+
+The two lists of instances describe which instances could be
+moved/migrated and which couldn't for some reason ("unsuccessful"). The
+union of the instances in the two lists must be equal to the set of
+instances given in the original request. The successful list of
+instances contains elements as follows::
+
+  (instance name, target group name, [chosen node names])
+
+The choice of names is simply for readability reasons (for example,
+Ganeti could log the computed solution in the job information) and for
+being able to check (manually) for consistency that the generated
+opcodes match the intended target groups/nodes. Note that for the
+node-evacuate operation, the group is not changed, but it should still
+be returned as such (as it's easier to have the same return type for
+both operations).
+
+The unsuccessful list of instances contains elements as follows::
+
+  (instance name, explanation)
+
+where ``explanation`` is a string describing why the plugin was not able
+to relocate the instance.
+
+The client is given a list of job IDs (see the :doc:`design for
+LU-generated jobs <design-lu-generated-jobs>`) which it can watch.
+Failures should be reported to the user.
+
+.. highlight:: python
+
+Example job list::
+
+  [
+    # First job
+    [
+      { "OP_ID": "OP_INSTANCE_MIGRATE",
+        "instance_name": "inst1.example.com",
+      },
+      { "OP_ID": "OP_INSTANCE_MIGRATE",
+        "instance_name": "inst2.example.com",
+      },
+    ],
+    # Second job
+    [
+      { "OP_ID": "OP_INSTANCE_REPLACE_DISKS",
+        "depends": [
+          [-1, ["success"]],
+          ],
+        "instance_name": "inst2.example.com",
+        "mode": "replace_new_secondary",
+        "remote_node": "node4.example.com",
+      },
+    ],
+    # Third job
+    [
+      { "OP_ID": "OP_INSTANCE_FAILOVER",
+        "depends": [
+          [-2, []],
+          ],
+        "instance_name": "inst8.example.com",
+      },
+    ],
+  ]
+
+Accepted opcodes:
+
+- ``OP_INSTANCE_FAILOVER``
+- ``OP_INSTANCE_MIGRATE``
+- ``OP_INSTANCE_REPLACE_DISKS``
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
diff --git a/doc/design-network.rst b/doc/design-network.rst
new file mode 100644 (file)
index 0000000..a7318d4
--- /dev/null
@@ -0,0 +1,313 @@
+==================
+Network management
+==================
+
+.. contents:: :depth: 4
+
+This is a design document detailing the implementation of network resource
+management in Ganeti.
+
+Current state and shortcomings
+==============================
+
+Currently Ganeti supports two configuration modes for instance NICs:
+routed and bridged mode. The ``ip`` NIC parameter, which is mandatory
+for routed NICs and optional for bridged ones, holds the given NIC's IP
+address and may be filled either manually, or via a DNS lookup for the
+instance's hostname.
+
+This approach presents some shortcomings:
+
+a) It relies on external systems to perform network resource
+   management. Although large organizations may already have IP pool
+   management software in place, this is not usually the case with
+   stand-alone deployments. For smaller installations it makes sense to
+   allocate a pool of IP addresses to Ganeti and let it transparently
+   assign these IPs to instances as appropriate.
+
+b) The NIC network information is incomplete, lacking netmask and
+   gateway.  Operating system providers could for example use the
+   complete network information to fully configure an instance's
+   network parameters upon its creation.
+
+   Furthermore, having full network configuration information would
+   enable Ganeti nodes to become more self-contained and be able to
+   infer system configuration (e.g. /etc/network/interfaces content)
+   from Ganeti configuration. This should make configuration of
+   newly-added nodes a lot easier and less dependant on external
+   tools/procedures.
+
+c) Instance placement must explicitly take network availability in
+   different node groups into account; the same ``link`` is implicitly
+   expected to connect to the same network across the whole cluster,
+   which may not always be the case with large clusters with multiple
+   node groups.
+
+
+Proposed changes
+----------------
+
+In order to deal with the above shortcomings, we propose to extend
+Ganeti with high-level network management logic, which consists of a new
+NIC mode called ``managed``, a new "Network" configuration object and
+logic to perform IP address pool management, i.e. maintain a set of
+available and occupied IP addresses.
+
+Configuration changes
++++++++++++++++++++++
+
+We propose the introduction of a new high-level Network object,
+containing (at least) the following data:
+
+- Symbolic name
+- UUID
+- Network in CIDR notation (IPv4 + IPv6)
+- Default gateway, if one exists (IPv4 + IPv6)
+- IP pool management data (reservations)
+- Default NIC connectivity mode (bridged, routed). This is the
+  functional equivalent of the current NIC ``mode``.
+- Default host interface (e.g. br0). This is the functional equivalent
+  of the current NIC ``link``.
+- Tags
+
+Each network will be connected to any number of node groups, possibly
+overriding connectivity mode and host interface for each node group.
+This is achieved by adding a ``networks`` slot to the NodeGroup object
+and using the networks' UUIDs as keys.
+
+IP pool management
+++++++++++++++++++
+
+A new helper library is introduced, wrapping around Network objects to
+give IP pool management capabilities. A network's pool is defined by two
+bitfields, the length of the network size each:
+
+``reservations``
+  This field holds all IP addresses reserved by Ganeti instances, as
+  well as cluster IP addresses (node addresses + cluster master)
+
+``external reservations``
+  This field holds all IP addresses that are manually reserved by the
+  administrator, because some other equipment is using them outside the
+  scope of Ganeti.
+
+The bitfields are implemented using the python-bitarray package for
+space efficiency and their binary value stored base64-encoded for JSON
+compatibility. This approach gives relatively compact representations
+even for large IPv4 networks (e.g. /20).
+
+Ganeti-owned IP addresses (node + master IPs) are reserved automatically
+if the cluster's data network itself is placed under pool management.
+
+Helper ConfigWriter methods provide free IP address generation and
+reservation, using a TemporaryReservationManager.
+
+It should be noted that IP pool management is performed only for IPv4
+networks, as they are expected to be densely populated. IPv6 networks
+can use different approaches, e.g. sequential address asignment or
+EUI-64 addresses.
+
+Managed NIC mode
+++++++++++++++++
+
+In order to be able to use the new network facility while maintaining
+compatibility with the current networking model, a new network mode is
+introduced, called ``managed`` to reflect the fact that the given NICs
+network configuration is managed by Ganeti itself. A managed mode NIC
+accepts the network it is connected to in its ``link`` argument.
+Userspace tools can refer to networks using their symbolic names,
+however internally, the link argument stores the network's UUID.
+
+We also introduce a new ``ip`` address value, ``constants.NIC_IP_POOL``,
+that specifies that a given NIC's IP address should be obtained using
+the IP address pool of the specified network. This value is only valid
+for managed-mode NICs, where it is also used as a default instead of
+``constants.VALUE_AUTO``. A managed-mode NIC's IP address can also be
+specified manually, as long as it is compatible with the network the NIC
+is connected to.
+
+
+Hooks
++++++
+
+``OP_NETWORK_ADD``
+  Add a network to Ganeti
+
+  :directory: network-add
+  :pre-execution: master node
+  :post-execution: master node
+
+``OP_NETWORK_CONNECT``
+  Connect a network to a node group. This hook can be used to e.g.
+  configure network interfaces on the group's nodes.
+
+  :directory: network-connect
+  :pre-execution: master node, all nodes in the connected group
+  :post-execution: master node, all nodes in the connected group
+
+``OP_NETWORK_DISCONNECT``
+  Disconnect a network to a node group. This hook can be used to e.g.
+  deconfigure network interfaces on the group's nodes.
+
+  :directory: network-disconnect
+  :pre-execution: master node, all nodes in the connected group
+  :post-execution: master node, all nodes in the connected group
+
+``OP_NETWORK_REMOVE``
+  Remove a network from Ganeti
+
+  :directory: network-add
+  :pre-execution: master node, all nodes
+  :post-execution: master node, all nodes
+
+Hook variables
+^^^^^^^^^^^^^^
+
+``INSTANCE_NICn_MANAGED``
+  Non-zero if NIC n is a managed-mode NIC
+
+``INSTANCE_NICn_NETWORK``
+  The friendly name of the network
+
+``INSTANCE_NICn_NETWORK_UUID``
+  The network's UUID
+
+``INSTANCE_NICn_NETWORK_TAGS``
+  The network's tags
+
+``INSTANCE_NICn_NETWORK_IPV4_CIDR``, ``INSTANCE_NICn_NETWORK_IPV6_CIDR``
+  The subnet in CIDR notation
+
+``INSTANCE_NICn_NETWORK_IPV4_GATEWAY``, ``INSTANCE_NICn_NETWORK_IPV6_GATEWAY``
+  The subnet's default gateway
+
+
+Backend changes
++++++++++++++++
+
+In order to keep the hypervisor-visible changes to a minimum, and
+maintain compatibility with the existing network configuration scripts,
+the instance's hypervisor configuration will have host-level link and
+mode replaced by the *connectivity mode* and *host interface* of the
+given network on the current node group.
+
+The managed mode can be detected by the presence of new environment
+variables in network configuration scripts:
+
+Network configuration script variables
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+``MANAGED``
+  Non-zero if NIC is a managed-mode NIC
+
+``NETWORK``
+  The friendly name of the network
+
+``NETWORK_UUID``
+  The network's UUID
+
+``NETWORK_TAGS``
+  The network's tags
+
+``NETWORK_IPv4_CIDR``, ``NETWORK_IPv6_CIDR``
+  The subnet in CIDR notation
+
+``NETWORK_IPV4_GATEWAY``, ``NETWORK_IPV6_GATEWAY``
+  The subnet's default gateway
+
+Userland interface
+++++++++++++++++++
+
+A new client script is introduced, ``gnt-network``, which handles
+network-related configuration in Ganeti.
+
+Network addition/deletion
+^^^^^^^^^^^^^^^^^^^^^^^^^
+::
+
+ gnt-network add --cidr=192.0.2.0/24 --gateway=192.0.2.1 \
+                --cidr6=2001:db8:2ffc::/64 --gateway6=2001:db8:2ffc::1 \
+                --nic_connectivity=bridged --host_interface=br0 public
+ gnt-network remove public (only allowed if no instances are using the network)
+
+Manual IP address reservation
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+::
+
+ gnt-network reserve-ips public 192.0.2.2 192.0.2.10-192.0.2.20
+ gnt-network release-ips public 192.0.2.3
+
+
+Network modification
+^^^^^^^^^^^^^^^^^^^^
+::
+
+ gnt-network modify --cidr=192.0.2.0/25 public (only allowed if all current reservations fit in the new network)
+ gnt-network modify --gateway=192.0.2.126 public
+ gnt-network modify --host_interface=test --nic_connectivity=routed public (issues warning about instances that need to be rebooted)
+ gnt-network rename public public2
+
+
+Assignment to node groups
+^^^^^^^^^^^^^^^^^^^^^^^^^
+::
+
+ gnt-network connect public nodegroup1
+ gnt-network connect --host_interface=br1 public nodegroup2
+ gnt-network disconnect public nodegroup1 (only permitted if no instances are currently using this network in the group)
+
+Tagging
+^^^^^^^
+::
+
+ gnt-network add-tags public foo bar:baz
+
+Network listing
+^^^^^^^^^^^^^^^
+::
+
+ gnt-network list
+  Name         IPv4 Network    IPv4 Gateway          IPv6 Network             IPv6 Gateway             Connected to
+  public        192.0.2.0/24   192.0.2.1       2001:db8:dead:beef::/64    2001:db8:dead:beef::1       nodegroup1:br0
+  private       10.0.1.0/24       -                     -                              -
+
+Network information
+^^^^^^^^^^^^^^^^^^^
+::
+
+ gnt-network info public
+  Name: public
+  IPv4 Network: 192.0.2.0/24
+  IPv4 Gateway: 192.0.2.1
+  IPv6 Network: 2001:db8:dead:beef::/64
+  IPv6 Gateway: 2001:db8:dead:beef::1
+  Total IPv4 count: 256
+  Free address count: 201 (80% free)
+  IPv4 pool status: XXX.........XXXXXXXXXXXXXX...XX.............
+                    XXX..........XXX...........................X
+                    ....XXX..........XXX.....................XXX
+                                            X: occupied  .: free
+  Externally reserved IPv4 addresses:
+    192.0.2.3, 192.0.2.22
+  Connected to node groups:
+   default (link br0), other_group(link br1)
+  Used by 22 instances:
+   inst1
+   inst2
+   inst32
+   ..
+
+
+IAllocator changes
+++++++++++++++++++
+
+The IAllocator protocol can be made network-aware, i.e. also consider
+network availability for node group selection. Networks, as well as
+future shared storage pools, can be seen as constraints used to rule out
+the placement on certain node groups.
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
index 1e78c1a..f6aebbc 100644 (file)
@@ -52,6 +52,32 @@ New ``gnt-cluster`` Parameter
 | Parameters: ``--oob-program``
 | Options: ``--oob-program``: executable OOB program (absolute path)
 
+New ``gnt-cluster epo`` Command
++++++++++++++++++++++++++++++++
+
+| Program: ``gnt-cluster``
+| Command: ``epo``
+| Parameter: ``--on`` ``--force`` ``--groups`` ``--all``
+| Options: ``--on``: By default epo turns off, with ``--on`` it tries to get the
+|                    cluster back online
+|          ``--force``: To force the operation without asking for confirmation
+|          ``--groups``: To operate on groups instead of nodes
+|          ``--all``: To operate on the whole cluster
+
+This is a convenience command to allow easy emergency power off of a whole
+cluster or part of it. It takes care of all steps needed to get the cluster into
+a sane state to turn off the nodes.
+
+With ``--on`` it does the reverse and tries to bring the rest of the cluster back
+to life.
+
+.. note::
+  The master node is not able to shut itself cleanly down. Therefore, this
+  command will not do all the work on single node clusters. On multi node
+  clusters the command tries to find another master or if that is not possible
+  prepares everything to the point where the user has to shutdown the master
+  node itself alone this applies also to the single node cluster configuration.
+
 New ``gnt-node`` Property
 +++++++++++++++++++++++++
 
@@ -360,3 +386,9 @@ The ``gnt-node power-[on|off]`` (power state changes) commands will create log
 entries following current Ganeti logging practices. In addition, health items
 with status WARNING or CRITICAL will be logged for each run of ``gnt-node
 health``.
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
diff --git a/doc/design-ovf-support.rst b/doc/design-ovf-support.rst
new file mode 100644 (file)
index 0000000..d238259
--- /dev/null
@@ -0,0 +1,166 @@
+==============================================================
+Ganeti Instance Import/Export using Open Virtualization Format
+==============================================================
+
+Background
+==========
+
+Open Virtualization Format is an open standard for packaging
+information regarding virtual machines. It is used, among other, by
+VMWare, VirtualBox and XenServer. OVF allows users to migrate between
+virtualization software without the need of reconfiguring hardware,
+network or operating system.
+
+Currently, exporting instance in Ganeti results with a configuration
+file that is readable only for Ganeti. It disallows the users to
+change the platform they use without loosing all the machine's
+configuration.  Import function in Ganeti is also currently limited to
+the previously prepared instances.
+
+Implementation of OVF support allows users to migrate to Ganeti from
+other platforms, thus potentially increasing the usage. It also
+enables virtual machine end-users to create their own machines
+(e.g. in VirtualBox or SUSE Studio) and then add them to Ganeti
+cluster, thus providing better personalization.
+
+Overview
+========
+
+Open Virtualization Format description
+--------------------------------------
+
+According to the DMTF document introducing the standard: "The Open
+Virtualization Format (OVF) Specification describes an open, secure,
+portable, efficient and extensible format for the packaging and
+distribution of software to be run in virtual machines."  OVF supports
+both single and multiple- configurations of VMs in one package, is
+host- and virtualization platform-independent and optimized for
+distribution (e.g. by allowing usage of public key infrastructure and
+providing tools for management of basic software licensing).
+
+There are no limitations regarding hard drive images used, as long as
+the description is provided. Any hardware described in a proper
+i.e. CIM - Common Information Model) format is accepted, although
+there is no guarantee that every virtualization software will support
+all types of hardware.
+
+OVF package should contain one file with .ovf extension, which is an
+XML file specifying the following (per virtual machine):
+
+- virtual disks
+- network description
+- list of virtual hardware
+- operating system, if any
+
+Each of the elements in .ovf file may, if desired, contain a
+human-readable description to every piece of information given.
+
+Additionally, the package may have some disk image files and other
+additional resources (e.g. ISO images).
+
+Supported disk formats
+----------------------
+
+Although OVF is claimed to support 'any disk format', what we are
+interested in is which of the formats are supported by VM managers
+that currently use OVF.
+
+- VMWare: ``.vmdk`` (which comes in at least 3 different flavours:
+  ``sparse``, ``compressed`` and ``streamOptimized``)
+- VirtualBox: ``.vdi`` (VirtualBox's format), ``.vmdk``, ``.vhd``
+  (Microsoft and XenServer); export disk format is always ``.vmdk``
+- XenServer: ``.vmdk``, ``.vhd``; export disk format is always
+  ``.vhd``
+- Red Hat Enterprise Virtualization: ``.raw`` (raw disk format),
+  ``.cow`` (qemu's ``QCOW2``)
+- other: AbiCloud, OpenNode Cloud, SUSE Studio, Morfeo Claudia,
+  OpenStack
+
+In our implementation of the OVF we plan to allow a choice between
+raw, cow and vmdk disk formats for both import and export. The
+justification is the following:
+
+- Raw format is supported as it is the main format of disk images used
+  in Ganeti, thus it is effortless to provide support for this format
+- Cow is used in Qemu, [TODO: ..why do we support it, again? That is,
+  if we do?]
+- Vmdk is most commonly supported in virtualization software, it also
+  has the advantage of producing relatively small disk images, which
+  is extremely important advantage when moving instances.
+
+The conversion between RAW and the other formats will be done using
+qemu-img, which transforms, among other, raw disk images to monolithic
+sparse vmdk images.
+
+
+Planned limitations
+===================
+
+The limitations regarding import of the OVF instances generated
+outside Ganeti will be (in general) the same, as limitations for
+Ganeti itself.  The desired behavior in case of encountering
+unsupported element will be to ignore this element's tag and inform
+the user on console output, if possible - without interruption of the
+import process.
+
+Package
+-------
+
+There are no limitations regarding support for multiple files in
+package or packing the ovf package into one OVA (Open Virtual
+Appliance) file.  As for certificates and licenses in the package,
+their support will be under discussion after completion of the basic
+features implementation.
+
+Multiple Virtual Systems
+------------------------
+
+At first only singular instances (i.e. VirtualSystem, not
+VirtualSystemCollection) will be supported. In the future multi-tiered
+appliances containing whole nodes (or even clusters) are considered an
+option.
+
+Disks
+-----
+
+As mentioned, Ganeti will allow exporting only ``raw``, ``cow`` and
+``vmdk`` formats.  As for import, we will support all that
+``qemu-img`` can convert to raw format. At this point this means
+``raw``, ``cow``, ``qcow``, ``qcow2``, ``vmdk`` and ``cloop``.  We do
+not plan for now to support ``vdi`` or ``vhd``.
+
+We support compression both for import and export - for export this
+will use ovftools with chosen level of compression. There is also a
+possibility to provide virtual disk in chunks of equal size.
+
+When no ``ovf:format`` tag is provided during import, we assume that
+the disk is to be created on import and proceed accordingly.
+
+Network
+-------
+
+There are no known limitations regarding network support.
+
+Hardware
+--------
+
+TODO
+
+Operating Systems
+-----------------
+
+TODO
+
+Other
+-----
+
+Implementation details
+======================
+
+TODO
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
index 8310ef1..a307e9f 100644 (file)
@@ -100,6 +100,8 @@ items:
   Jobs
 ``lock``
   Locks
+``os``
+  Operating systems
 
 .. _data-query:
 
@@ -284,6 +286,10 @@ A field definition is a dictionary with the following entries:
   formatting any unknown types the same way as "other", which should be
   a string representation in most cases.
 
+``doc`` (string)
+  Human-readable description. Must start with uppercase character and
+  must not end with punctuation or contain newlines.
+
 .. TODO: Investigate whether there are fields with floating point
 .. numbers
 
@@ -322,7 +328,7 @@ methods.  Unavailable values are set to ``None``. If unknown fields were
 requested, the whole query fails as the client expects exactly the
 fields it requested.
 
-.. _luxi:
+.. _query2-luxi:
 
 LUXI
 ++++
diff --git a/doc/design-shared-storage.rst b/doc/design-shared-storage.rst
new file mode 100644 (file)
index 0000000..9f3a0ea
--- /dev/null
@@ -0,0 +1,280 @@
+======================================
+Ganeti shared storage support for 2.3+
+======================================
+
+This document describes the changes in Ganeti 2.3+ compared to Ganeti
+2.3 storage model.
+
+.. contents:: :depth: 4
+
+Objective
+=========
+
+The aim is to introduce support for externally mirrored, shared storage.
+This includes two distinct disk templates:
+
+- A shared filesystem containing instance disks as regular files
+  typically residing on a networked or cluster filesystem (e.g. NFS,
+  AFS, Ceph, OCFS2, etc.).
+- Instance images being shared block devices, typically LUNs residing on
+  a SAN appliance.
+
+Background
+==========
+DRBD is currently the only shared storage backend supported by Ganeti.
+DRBD offers the advantages of high availability while running on
+commodity hardware at the cost of high network I/O for block-level
+synchronization between hosts. DRBD's master-slave model has greatly
+influenced Ganeti's design, primarily by introducing the concept of
+primary and secondary nodes and thus defining an instance's “mobility
+domain”.
+
+Although DRBD has many advantages, many sites choose to use networked
+storage appliances for Virtual Machine hosting, such as SAN and/or NAS,
+which provide shared storage without the administrative overhead of DRBD
+nor the limitation of a 1:1 master-slave setup. Furthermore, new
+distributed filesystems such as Ceph are becoming viable alternatives to
+expensive storage appliances. Support for both modes of operation, i.e.
+shared block storage and shared file storage backend would make Ganeti a
+robust choice for high-availability virtualization clusters.
+
+Throughout this document, the term “externally mirrored storage” will
+refer to both modes of shared storage, suggesting that Ganeti does not
+need to take care about the mirroring process from one host to another.
+
+Use cases
+=========
+We consider the following use cases:
+
+- A virtualization cluster with FibreChannel shared storage, mapping at
+  leaste one LUN per instance, accessible by the whole cluster.
+- A virtualization cluster with instance images stored as files on an
+  NFS server.
+- A virtualization cluster storing instance images on a Ceph volume.
+
+Design Overview
+===============
+
+The design addresses the following procedures:
+
+- Refactoring of all code referring to constants.DTS_NET_MIRROR.
+- Obsolescence of the primary-secondary concept for externally mirrored
+  storage.
+- Introduction of a shared file storage disk template for use with networked
+  filesystems.
+- Introduction of shared block device disk template with device
+  adoption.
+
+Additionally, mid- to long-term goals include:
+
+- Support for external “storage pools”.
+- Introduction of an interface for communicating with external scripts,
+  providing methods for the various stages of a block device's and
+  instance's life-cycle. In order to provide storage provisioning
+  capabilities for various SAN appliances, external helpers in the form
+  of a “storage driver” will be possibly introduced as well.
+
+Refactoring of all code referring to constants.DTS_NET_MIRROR
+=============================================================
+
+Currently, all storage-related decision-making depends on a number of
+frozensets in lib/constants.py, typically constants.DTS_NET_MIRROR.
+However, constants.DTS_NET_MIRROR is used to signify two different
+attributes:
+
+- A storage device that is shared
+- A storage device whose mirroring is supervised by Ganeti
+
+We propose the introduction of two new frozensets to ease
+decision-making:
+
+- constants.DTS_EXT_MIRROR, holding externally mirrored disk templates
+- constants.DTS_MIRRORED, being a union of constants.DTS_EXT_MIRROR and
+  DTS_NET_MIRROR.
+
+Additionally, DTS_NET_MIRROR will be renamed to DTS_INT_MIRROR to reflect
+the status of the storage as internally mirrored by Ganeti.
+
+Thus, checks could be grouped into the following categories:
+
+- Mobility checks, like whether an instance failover or migration is
+  possible should check against constants.DTS_MIRRORED
+- Syncing actions should be performed only for templates in
+  constants.DTS_NET_MIRROR
+
+Obsolescence of the primary-secondary node model
+================================================
+
+The primary-secondary node concept has primarily evolved through the use
+of DRBD. In a globally shared storage framework without need for
+external sync (e.g. SAN, NAS, etc.), such a notion does not apply for the
+following reasons:
+
+1. Access to the storage does not necessarily imply different roles for
+   the nodes (e.g. primary vs secondary).
+2. The same storage is available to potentially more than 2 nodes. Thus,
+   an instance backed by a SAN LUN for example may actually migrate to
+   any of the other nodes and not just a pre-designated failover node.
+
+The proposed solution is using the iallocator framework for run-time
+decision making during migration and failover, for nodes with disk
+templates in constants.DTS_EXT_MIRROR. Modifications to gnt-instance and
+gnt-node will be required to accept target node and/or iallocator
+specification for these operations. Modifications of the iallocator
+protocol will be required to address at least the following needs:
+
+- Allocation tools must be able to distinguish between internal and
+  external storage
+- Migration/failover decisions must take into account shared storage
+  availability
+
+Introduction of a shared file disk template
+===========================================
+
+Basic shared file storage support can be implemented by creating a new
+disk template based on the existing FileStorage class, with only minor
+modifications in lib/bdev.py. The shared file disk template relies on a
+shared filesystem (e.g. NFS, AFS, Ceph, OCFS2 over SAN or DRBD) being
+mounted on all nodes under the same path, where instance images will be
+saved.
+
+A new cluster initialization option is added to specify the mountpoint
+of the shared filesystem.
+
+The remainder of this document deals with shared block storage.
+
+Introduction of a shared block device template
+==============================================
+
+Basic shared block device support will be implemented with an additional
+disk template. This disk template will not feature any kind of storage
+control (provisioning, removal, resizing, etc.), but will instead rely
+on the adoption of already-existing block devices (e.g. SAN LUNs, NBD
+devices, remote iSCSI targets, etc.).
+
+The shared block device template will make the following assumptions:
+
+- The adopted block device has a consistent name across all nodes,
+  enforced e.g. via udev rules.
+- The device will be available with the same path under all nodes in the
+  node group.
+
+Long-term shared storage goals
+==============================
+Storage pool handling
+---------------------
+
+A new cluster configuration attribute will be introduced, named
+“storage_pools”, modeled as a dictionary mapping storage pools to
+external storage drivers (see below), e.g.::
+
+ {
+  "nas1": "foostore",
+  "nas2": "foostore",
+  "cloud1": "barcloud",
+ }
+
+Ganeti will not interpret the contents of this dictionary, although it
+will provide methods for manipulating them under some basic constraints
+(pool identifier uniqueness, driver existence). The manipulation of
+storage pools will be performed by implementing new options to the
+`gnt-cluster` command::
+
+ gnt-cluster modify --add-pool nas1 foostore
+ gnt-cluster modify --remove-pool nas1 # There may be no instances using
+                                       # the pool to remove it
+
+Furthermore, the storage pools will be used to indicate the availability
+of storage pools to different node groups, thus specifying the
+instances' “mobility domain”.
+
+New disk templates will also be necessary to facilitate the use of external
+storage. The proposed addition is a whole template namespace created by
+prefixing the pool names with a fixed string, e.g. “ext:”, forming names
+like “ext:nas1”, “ext:foo”.
+
+Interface to the external storage drivers
+-----------------------------------------
+
+In addition to external storage pools, a new interface will be
+introduced to allow external scripts to provision and manipulate shared
+storage.
+
+In order to provide storage provisioning and manipulation (e.g. growing,
+renaming) capabilities, each instance's disk template can possibly be
+associated with an external “storage driver” which, based on the
+instance's configuration and tags, will perform all supported storage
+operations using auxiliary means (e.g. XML-RPC, ssh, etc.).
+
+A “storage driver” will have to provide the following methods:
+
+- Create a disk
+- Remove a disk
+- Rename a disk
+- Resize a disk
+- Attach a disk to a given node
+- Detach a disk from a given node
+
+The proposed storage driver architecture borrows heavily from the OS
+interface and follows a one-script-per-function approach. A storage
+driver is expected to provide the following scripts:
+
+- `create`
+- `resize`
+- `rename`
+- `remove`
+- `attach`
+- `detach`
+
+These executables will be called once for each disk with no arguments
+and all required information will be passed through environment
+variables. The following environment variables will always be present on
+each invocation:
+
+- `INSTANCE_NAME`: The instance's name
+- `INSTANCE_UUID`: The instance's UUID
+- `INSTANCE_TAGS`: The instance's tags
+- `DISK_INDEX`: The current disk index.
+- `LOGICAL_ID`: The disk's logical id (if existing)
+- `POOL`: The storage pool the instance belongs to.
+
+Additional variables may be available in a per-script context (see
+below).
+
+Of particular importance is the disk's logical ID, which will act as
+glue between Ganeti and the external storage drivers; there are two
+possible ways of using a disk's logical ID in a storage driver:
+
+1. Simply use it as a unique identifier (e.g. UUID) and keep a separate,
+   external database linking it to the actual storage.
+2. Encode all useful storage information in the logical ID and have the
+   driver decode it at runtime.
+
+All scripts should return 0 on success and non-zero on error accompanied by
+an appropriate error message on stderr. Furthermore, the following
+special cases are defined:
+
+1. `create` In case of success, a string representing the disk's logical
+   id must be returned on stdout, which will be saved in the instance's
+   configuration and can be later used by the other scripts of the same
+   storage driver. The logical id may be based on instance name,
+   instance uuid and/or disk index.
+
+   Additional environment variables present:
+     - `DISK_SIZE`: The requested disk size in MiB
+
+2. `resize` In case of success, output the new disk size.
+
+   Additional environment variables present:
+     - `DISK_SIZE`: The requested disk size in MiB
+
+3. `rename` On success, a new logical id should be returned, which will
+   replace the old one. This script is meant to rename the instance's
+   backing store and update the disk's logical ID in case one of them is
+   bound to the instance name.
+
+   Additional environment variables present:
+     - `NEW_INSTANCE_NAME`: The instance's new name.
+
+
+.. vim: set textwidth=72 :
diff --git a/doc/design-x509-ca.rst b/doc/design-x509-ca.rst
new file mode 100644 (file)
index 0000000..a47a7fb
--- /dev/null
@@ -0,0 +1,204 @@
+=======================================
+Design for a X509 Certificate Authority
+=======================================
+
+.. contents:: :depth: 4
+
+Current state and shortcomings
+------------------------------
+
+Import/export in Ganeti have a need for many unique X509 certificates.
+So far these were all self-signed, but with the :doc:`new design for
+import/export <design-impexp2>` they need to be signed by a Certificate
+Authority (CA).
+
+
+Proposed changes
+----------------
+
+The plan is to implement a simple CA in Ganeti.
+
+Interacting with an external CA is too difficult or impossible for
+automated processes like exporting instances, so each Ganeti cluster
+will have its own CA. The public key will be stored in
+``…/lib/ganeti/ca/cert.pem``, the private key (only readable by the
+master daemon) in ``…/lib/ganeti/ca/key.pem``.
+
+Similar to the RAPI certificate, a new CA certificate can be installed
+using the ``gnt-cluster renew-crypto`` command. Such a CA could be an
+intermediate of a third-party CA. By default a self-signed CA is
+generated and used.
+
+.. _x509-ca-serial:
+
+Each certificate signed by the CA is required to have a unique serial
+number. The serial number is stored in the file
+``…/lib/ganeti/ca/serial``, replicated to all master candidates and
+never reset, even when a new CA is installed.
+
+The threat model is expected to be the same as with self-signed
+certificates. To reinforce this, all certificates signed by the CA must
+be valid for less than one week (168 hours).
+
+Implementing support for Certificate Revocation Lists (CRL) using
+OpenSSL is non-trivial. Lighttpd doesn't support them at all and
+`apparently never will in version 1.4.x
+<http://redmine.lighttpd.net/issues/2278>`_. Some CRL-related parts have
+only been added in the most recent version of pyOpenSSL (0.11). Instead
+of a CRL, Ganeti will gain a new cluster configuration property defining
+the minimum accepted serial number. In case of a lost or compromised
+private key this property can be set to the most recently generated
+serial number.
+
+While possible to implement in the future, other X509 certificates used
+by the cluster (e.g. RAPI or inter-node communication) will not be
+automatically signed by the per-cluster CA.
+
+The ``commonName`` attribute of signed certificates must be set to the
+the cluster name or the name of a node in the cluster.
+
+
+Software requirements
+---------------------
+
+- pyOpenSSL 0.10 or above (lower versions can't set the X509v3 extension
+  ``subjectKeyIdentifier`` recommended for certificate authority
+  certificates by :rfc:`3280`, section 4.2.1.2)
+
+
+Code samples
+------------
+
+Generating X509 CA using pyOpenSSL
+++++++++++++++++++++++++++++++++++
+
+.. highlight:: python
+
+The following code sample shows how to generate a CA certificate using
+pyOpenSSL::
+
+  key = OpenSSL.crypto.PKey()
+  key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
+
+  ca = OpenSSL.crypto.X509()
+  ca.set_version(3)
+  ca.set_serial_number(1)
+  ca.get_subject().CN = "ca.example.com"
+  ca.gmtime_adj_notBefore(0)
+  ca.gmtime_adj_notAfter(24 * 60 * 60)
+  ca.set_issuer(ca.get_subject())
+  ca.set_pubkey(key)
+  ca.add_extensions([
+    OpenSSL.crypto.X509Extension("basicConstraints", True,
+                                 "CA:TRUE, pathlen:0"),
+    OpenSSL.crypto.X509Extension("keyUsage", True,
+                                 "keyCertSign, cRLSign"),
+    OpenSSL.crypto.X509Extension("subjectKeyIdentifier", False, "hash",
+                                 subject=ca),
+    ])
+  ca.sign(key, "sha1")
+
+
+Signing X509 certificate using CA
++++++++++++++++++++++++++++++++++
+
+.. highlight:: python
+
+The following code sample shows how to sign an X509 certificate using a
+CA::
+
+  ca_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
+                                            "ca.pem")
+  ca_key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM,
+                                          "ca.pem")
+
+  key = OpenSSL.crypto.PKey()
+  key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
+
+  cert = OpenSSL.crypto.X509()
+  cert.get_subject().CN = "node1.example.com"
+  cert.set_serial_number(1)
+  cert.gmtime_adj_notBefore(0)
+  cert.gmtime_adj_notAfter(24 * 60 * 60)
+  cert.set_issuer(ca_cert.get_subject())
+  cert.set_pubkey(key)
+  cert.sign(ca_key, "sha1")
+
+
+How to generate Certificate Signing Request
++++++++++++++++++++++++++++++++++++++++++++
+
+.. highlight:: python
+
+The following code sample shows how to generate an X509 Certificate
+Request (CSR)::
+
+  key = OpenSSL.crypto.PKey()
+  key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
+
+  req = OpenSSL.crypto.X509Req()
+  req.get_subject().CN = "node1.example.com"
+  req.set_pubkey(key)
+  req.sign(key, "sha1")
+
+  # Write private key
+  print OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
+
+  # Write request
+  print OpenSSL.crypto.dump_certificate_request(OpenSSL.crypto.FILETYPE_PEM, req)
+
+
+X509 certificate from Certificate Signing Request
++++++++++++++++++++++++++++++++++++++++++++++++++
+
+.. highlight:: python
+
+The following code sample shows how to create an X509 certificate from a
+Certificate Signing Request and sign it with a CA::
+
+  ca_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
+                                            "ca.pem")
+  ca_key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM,
+                                          "ca.pem")
+  req = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM,
+                                                open("req.csr").read())
+
+  cert = OpenSSL.crypto.X509()
+  cert.set_subject(req.get_subject())
+  cert.set_serial_number(1)
+  cert.gmtime_adj_notBefore(0)
+  cert.gmtime_adj_notAfter(24 * 60 * 60)
+  cert.set_issuer(ca_cert.get_subject())
+  cert.set_pubkey(req.get_pubkey())
+  cert.sign(ca_key, "sha1")
+
+  print OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
+
+
+Verify whether X509 certificate matches private key
++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+.. highlight:: python
+
+The code sample below shows how to check whether a certificate matches
+with a certain private key. OpenSSL has a function for this,
+``X509_check_private_key``, but pyOpenSSL provides no access to it.
+
+::
+
+  ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD)
+  ctx.use_privatekey(key)
+  ctx.use_certificate(cert)
+  try:
+    ctx.check_privatekey()
+  except OpenSSL.SSL.Error:
+    print "Incorrect key"
+  else:
+    print "Key matches certificate"
+
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
index 8b58ed7..35d223c 100644 (file)
@@ -4,7 +4,7 @@ Developer notes
 Build dependencies
 ------------------
 
-Most dependencies from :doc:`install-quick`, plus:
+Most dependencies from :doc:`install-quick`, plus (for Python):
 
 - `GNU make <http://www.gnu.org/software/make/>`_
 - `GNU tar <http://www.gnu.org/software/tar/>`_
@@ -14,10 +14,38 @@ Most dependencies from :doc:`install-quick`, plus:
   (tested with version 0.6.1)
 - `graphviz <http://www.graphviz.org/>`_
 - the `en_US.UTF-8` locale must be enabled on the system
+- `pylint <http://www.logilab.org/857>`_ and its associated
+  dependencies
+
+Note that for pylint, at the current moment the following versions
+need to be used::
+
+    $ pylint --version
+    pylint 0.21.1,
+    astng 0.20.1, common 0.50.3
 
 To generate unittest coverage reports (``make coverage``), `coverage
 <http://pypi.python.org/pypi/coverage>`_ needs to be installed.
 
+For Haskell development, again all things from the quick install
+document, plus:
+
+- `haddock <http://www.haskell.org/haddock/>`_, documentation
+  generator (equivalent to epydoc for Python)
+- `HsColour <http://hackage.haskell.org/package/hscolour>`_, again
+  used for documentation (it's source-code pretty-printing)
+- `hlint <http://community.haskell.org/~ndm/hlint/>`_, a source code
+  linter (equivalent to pylint for Python)
+- the `QuickCheck <http://hackage.haskell.org/package/QuickCheck>`_
+  library, version 2.x
+- ``hpc``, which comes with the compiler, so you should already have
+  it
+
+Under Debian, these can be installed (on top of the required ones from
+the quick install document) via::
+
+  apt-get install libghc-quickcheck2-dev hscolour hlint
+
 
 Configuring for development
 ---------------------------
@@ -47,7 +75,7 @@ before use.
 
 This script, in the source code as ``daemons/daemon-util.in``, is used
 to start/stop Ganeti and do a few other things related to system
-daemons. Is is recommended to use ``daemon-util`` also from the system's
+daemons. It is recommended to use ``daemon-util`` also from the system's
 init scripts. That way the code starting and stopping daemons is shared
 and future changes have to be made in only one place.
 
index 484ff3f..a76eb95 100644 (file)
@@ -64,7 +64,7 @@ have been run.
 Naming
 ~~~~~~
 
-The allowed names for the scripts consist of (similar to *run-parts* )
+The allowed names for the scripts consist of (similar to *run-parts*)
 upper and lower case, digits, underscores and hyphens. In other words,
 the regexp ``^[a-zA-Z0-9_-]+$``. Also, non-executable scripts will be
 ignored.
@@ -213,6 +213,16 @@ Renames a node group.
 :pre-execution: master node and all nodes in the group
 :post-execution: master node and all nodes in the group
 
+OP_GROUP_EVACUATE
++++++++++++++++++
+
+Evacuates a node group.
+
+:directory: group-evacuate
+:env. vars: GROUP_NAME, TARGET_GROUPS
+:pre-execution: master node and all nodes in the group
+:post-execution: master node and all nodes in the group
+
 
 Instance operations
 ~~~~~~~~~~~~~~~~~~~
@@ -419,10 +429,10 @@ operation and not after its completion.
 :pre-execution: none
 :post-execution: master node
 
-OP_CLUSTER_VERIFY
-+++++++++++++++++
+OP_CLUSTER_VERIFY_GROUP
++++++++++++++++++++++++
 
-Verifies the cluster status. This is a special LU with regard to
+Verifies all nodes in a group. This is a special LU with regard to
 hooks, as the result of the opcode will be combined with the result of
 post-execution hooks, in order to allow administrators to enhance the
 cluster verification procedure.
@@ -430,7 +440,7 @@ cluster verification procedure.
 :directory: cluster-verify
 :env. vars: CLUSTER, MASTER, CLUSTER_TAGS, NODE_TAGS_<name>
 :pre-execution: none
-:post-execution: all nodes
+:post-execution: all nodes in a group
 
 OP_CLUSTER_RENAME
 +++++++++++++++++
@@ -468,8 +478,10 @@ anymore in Ganeti 2.0:
 Environment variables
 ---------------------
 
-Note that all variables listed here are actually prefixed with
-*GANETI_* in order to provide a clear namespace.
+Note that all variables listed here are actually prefixed with *GANETI_*
+in order to provide a clear namespace. In addition, post-execution
+scripts receive another set of variables, prefixed with *GANETI_POST_*,
+representing the status after the opcode executed.
 
 Common variables
 ~~~~~~~~~~~~~~~~
@@ -579,6 +591,9 @@ MASTER_CAPABLE
 VM_CAPABLE
   Whether the node can host instances.
 
+INSTANCE_TAGS
+  A space-delimited list of the instance's tags.
+
 NODE_NAME
   The target node of this operation (not the node on which the hook
   runs).
index ed7e6fb..b8c752c 100644 (file)
@@ -1,7 +1,7 @@
 Ganeti automatic instance allocation
 ====================================
 
-Documents Ganeti version 2.1
+Documents Ganeti version 2.4
 
 .. contents::
 
@@ -68,7 +68,14 @@ Input message
 ~~~~~~~~~~~~~
 
 The input message will be the JSON encoding of a dictionary containing
-the following:
+all the required information to perform the operation. We explain the
+contents of this dictionary in two parts: common information that every
+type of operation requires, and operation-specific information.
+
+Common information
+++++++++++++++++++
+
+All input dictionaries to the IAllocator must carry the following keys:
 
 version
   the version of the protocol; this document
@@ -84,92 +91,9 @@ enabled_hypervisors
   the list of enabled hypervisors
 
 request
-  a dictionary containing the request data:
-
-  type
-    the request type; this can be either ``allocate``, ``relocate`` or
-    ``multi-evacuate``; the ``allocate`` request is used when a new
-    instance needs to be placed on the cluster, while the ``relocate``
-    request is used when an existing instance needs to be moved within
-    the cluster; the ``multi-evacuate`` protocol requests that the
-    script computes the optimal relocate solution for all secondary
-    instances of the given nodes
-
-  The following keys are needed in allocate/relocate mode:
-
-  name
-    the name of the instance; if the request is a realocation, then this
-    name will be found in the list of instances (see below), otherwise
-    is the FQDN of the new instance
-
-  required_nodes
-    how many nodes should the algorithm return; while this information
-    can be deduced from the instace's disk template, it's better if
-    this computation is left to Ganeti as then allocator scripts are
-    less sensitive to changes to the disk templates
-
-  disk_space_total
-    the total disk space that will be used by this instance on the
-    (new) nodes; again, this information can be computed from the list
-    of instance disks and its template type, but Ganeti is better
-    suited to compute it
-
-  If the request is an allocation, then there are extra fields in the
-  request dictionary:
-
-  disks
-    list of dictionaries holding the disk definitions for this
-    instance (in the order they are exported to the hypervisor):
-
-    mode
-      either ``ro`` or ``rw`` denoting if the disk is read-only or
-      writable
-
-    size
-      the size of this disk in mebibytes
-
-  nics
-    a list of dictionaries holding the network interfaces for this
-    instance, containing:
-
-    ip
-      the IP address that Ganeti know for this instance, or null
-
-    mac
-      the MAC address for this interface
-
-    bridge
-      the bridge to which this interface will be connected
-
-  vcpus
-    the number of VCPUs for the instance
-
-  disk_template
-    the disk template for the instance
-
-  memory
-   the memory size for the instance
-
-  os
-   the OS type for the instance
-
-  tags
-    the list of the instance's tags
-
-  hypervisor
-    the hypervisor of this instance
-
-
-  If the request is of type relocate, then there is one more entry in
-  the request dictionary, named ``relocate_from``, and it contains a
-  list of nodes to move the instance away from; note that with Ganeti
-  2.0, this list will always contain a single node, the current
-  secondary of the instance.
-
-  The multi-evacuate mode has instead a single request argument:
-
-  nodes
-    the names of the nodes to be evacuated
+  a dictionary containing the details of the request; the keys vary
+  depending on the type of operation that's being requested, as
+  explained in `Operation-specific input`_ below.
 
 nodegroups
   a dictionary with the data for the cluster's node groups; it is keyed
@@ -179,7 +103,8 @@ nodegroups
   name
     the node group name
   alloc_policy
-    the allocation policy of the node group
+    the allocation policy of the node group (consult the semantics of
+    this attribute in the :manpage:`gnt-group(8)` manpage)
 
 instances
   a dictionary with the data for the current existing instance on the
@@ -253,6 +178,135 @@ nodes
    reserved_memory, free_memory, total_disk, free_disk, total_cpus,
    i_pri_memory and i_pri_up memory will be absent
 
+Operation-specific input
+++++++++++++++++++++++++
+
+All input dictionaries to the IAllocator carry, in the ``request``
+dictionary, detailed information about the operation that's being
+requested. The required keys vary depending on the type of operation, as
+follows.
+
+In all cases, it includes:
+
+  type
+    the request type; this can be either ``allocate``, ``relocate``,
+    ``change-group``, ``node-evacuate`` or ``multi-evacuate``. The
+    ``allocate`` request is used when a new instance needs to be placed
+    on the cluster. The ``relocate`` request is used when an existing
+    instance needs to be moved within its node group.
+
+    The ``multi-evacuate`` protocol used to request that the script
+    computes the optimal relocate solution for all secondary instances
+    of the given nodes. It is now deprecated and should no longer be
+    used.
+
+    The ``change-group`` request is used to relocate multiple instances
+    across multiple node groups. ``node-evacuate`` evacuates instances
+    off their node(s). These are described in a separate :ref:`design
+    document <multi-reloc-detailed-design>`.
+
+For both allocate and relocate mode, the following extra keys are needed
+in the ``request`` dictionary:
+
+  name
+    the name of the instance; if the request is a realocation, then this
+    name will be found in the list of instances (see below), otherwise
+    is the FQDN of the new instance; type *string*
+
+  required_nodes
+    how many nodes should the algorithm return; while this information
+    can be deduced from the instace's disk template, it's better if
+    this computation is left to Ganeti as then allocator scripts are
+    less sensitive to changes to the disk templates; type *integer*
+
+  disk_space_total
+    the total disk space that will be used by this instance on the
+    (new) nodes; again, this information can be computed from the list
+    of instance disks and its template type, but Ganeti is better
+    suited to compute it; type *integer*
+
+.. pyassert::
+
+   constants.DISK_ACCESS_SET == set([constants.DISK_RDONLY,
+     constants.DISK_RDWR])
+
+Allocation needs, in addition:
+
+  disks
+    list of dictionaries holding the disk definitions for this
+    instance (in the order they are exported to the hypervisor):
+
+    mode
+      either :pyeval:`constants.DISK_RDONLY` or
+      :pyeval:`constants.DISK_RDWR` denoting if the disk is read-only or
+      writable
+
+    size
+      the size of this disk in mebibytes
+
+  nics
+    a list of dictionaries holding the network interfaces for this
+    instance, containing:
+
+    ip
+      the IP address that Ganeti know for this instance, or null
+
+    mac
+      the MAC address for this interface
+
+    bridge
+      the bridge to which this interface will be connected
+
+  vcpus
+    the number of VCPUs for the instance
+
+  disk_template
+    the disk template for the instance
+
+  memory
+   the memory size for the instance
+
+  os
+   the OS type for the instance
+
+  tags
+    the list of the instance's tags
+
+  hypervisor
+    the hypervisor of this instance
+
+Relocation:
+
+  relocate_from
+     a list of nodes to move the instance away from (note that with
+     Ganeti 2.0, this list will always contain a single node, the
+     current secondary of the instance); type *list of strings*
+
+As for ``node-evacuate``, it needs the following request arguments:
+
+  instances
+    a list of instance names to evacuate; type *list of strings*
+
+  evac_mode
+    specify which instances to evacuate; one of ``primary-only``,
+    ``secondary-only``, ``all``, type *string*
+
+``change-group`` needs the following request arguments:
+
+  instances
+    a list of instance names whose group to change; type
+    *list of strings*
+
+  target_groups
+    must either be the empty list, or contain a list of group UUIDs that
+    should be considered for relocating instances to; type
+    *list of strings*
+
+Finally, in the case of multi-evacuate, there's one single request
+argument (in addition to ``type``):
+
+  evac_nodes
+    the names of the nodes to be evacuated; type *list of strings*
 
 Response message
 ~~~~~~~~~~~~~~~~
@@ -276,6 +330,11 @@ result
   entry in the input message, otherwise Ganeti will consider the result
   as failed
 
+  for the ``node-evacuate`` and ``change-group`` modes, this is a
+  dictionary containing, among other information, a list of lists of
+  serialized opcodes; see the :ref:`design document
+  <multi-reloc-result>` for a detailed description
+
   for multi-evacuation mode, this is a list of lists; each element of
   the list is a list of instance name and the new secondary node
 
@@ -289,42 +348,22 @@ Examples
 Input messages to scripts
 ~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Input message, new instance allocation::
+Input message, new instance allocation (common elements are listed this
+time, but not included in further examples below)::
 
   {
+    "version": 2,
+    "cluster_name": "cluster1.example.com",
     "cluster_tags": [],
-    "request": {
-      "required_nodes": 2,
-      "name": "instance3.example.com",
-      "tags": [
-        "type:test",
-        "owner:foo"
-      ],
-      "type": "allocate",
-      "disks": [
-        {
-          "mode": "w",
-          "size": 1024
-        },
-        {
-          "mode": "w",
-          "size": 2048
-        }
-      ],
-      "nics": [
-        {
-          "ip": null,
-          "mac": "00:11:22:33:44:55",
-          "bridge": null
-        }
-      ],
-      "vcpus": 1,
-      "disk_template": "drbd",
-      "memory": 2048,
-      "disk_space_total": 3328,
-      "os": "debootstrap+default"
+    "enabled_hypervisors": [
+      "xen-pvm"
+    ],
+    "nodegroups": {
+      "f4e06e0d-528a-4963-a5ad-10f3e114232d": {
+        "name": "default",
+        "alloc_policy": "preferred"
+      }
     },
-    "cluster_name": "cluster1.example.com",
     "instances": {
       "instance1.example.com": {
         "tags": [],
@@ -384,13 +423,13 @@ Input message, new instance allocation::
         "os": "debootstrap+default"
       }
     },
-    "version": 1,
     "nodes": {
       "node1.example.com": {
         "total_disk": 858276,
         "primary_ip": "198.51.100.1",
         "secondary_ip": "192.0.2.1",
         "tags": [],
+        "group": "f4e06e0d-528a-4963-a5ad-10f3e114232d",
         "free_memory": 3505,
         "free_disk": 856740,
         "total_memory": 4095
@@ -400,6 +439,7 @@ Input message, new instance allocation::
         "primary_ip": "198.51.100.2",
         "secondary_ip": "192.0.2.2",
         "tags": ["test"],
+        "group": "f4e06e0d-528a-4963-a5ad-10f3e114232d",
         "free_memory": 3505,
         "free_disk": 848320,
         "total_memory": 4095
@@ -409,35 +449,74 @@ Input message, new instance allocation::
         "primary_ip": "198.51.100.3",
         "secondary_ip": "192.0.2.3",
         "tags": [],
+        "group": "f4e06e0d-528a-4963-a5ad-10f3e114232d",
         "free_memory": 3505,
         "free_disk": 570648,
         "total_memory": 4095
       }
+    },
+    "request": {
+      "type": "allocate",
+      "name": "instance3.example.com",
+      "required_nodes": 2,
+      "disk_space_total": 3328,
+      "disks": [
+        {
+          "mode": "w",
+          "size": 1024
+        },
+        {
+          "mode": "w",
+          "size": 2048
+        }
+      ],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "00:11:22:33:44:55",
+          "bridge": null
+        }
+      ],
+      "vcpus": 1,
+      "disk_template": "drbd",
+      "memory": 2048,
+      "os": "debootstrap+default",
+      "tags": [
+        "type:test",
+        "owner:foo"
+      ],
+      hypervisor: "xen-pvm"
     }
   }
 
-Input message, reallocation. Since only the request entry in the input
-message is changed, we show only this changed entry::
-
-  "request": {
-    "relocate_from": [
-      "node3.example.com"
-    ],
-    "required_nodes": 1,
-    "type": "relocate",
-    "name": "instance2.example.com",
-    "disk_space_total": 832
-  },
+Input message, reallocation::
 
+  {
+    "version": 2,
+    ...
+    "request": {
+      "type": "relocate",
+      "name": "instance2.example.com",
+      "required_nodes": 1,
+      "disk_space_total": 832,
+      "relocate_from": [
+        "node3.example.com"
+      ]
+    }
+  }
 
 Input message, node evacuation::
 
-  "request": {
-    "evac_nodes": [
-      "node2"
-    ],
-    "type": "multi-evacuate"
-  },
+  {
+    "version": 2,
+    ...
+    "request": {
+      "type": "multi-evacuate",
+      "evac_nodes": [
+        "node2"
+      ],
+    }
+  }
 
 
 Response messages
@@ -445,25 +524,26 @@ Response messages
 Successful response message::
 
   {
+    "success": true,
     "info": "Allocation successful",
     "result": [
       "node2.example.com",
       "node1.example.com"
-    ],
-    "success": true
+    ]
   }
 
 Failed response message::
 
   {
+    "success": false,
     "info": "Can't find a suitable node for position 2 (already selected: node2.example.com)",
-    "result": [],
-    "success": false
+    "result": []
   }
 
 Successful node evacuation message::
 
   {
+    "success": true,
     "info": "Request successful",
     "result": [
       [
@@ -474,8 +554,7 @@ Successful node evacuation message::
         "instance2",
         "node1"
       ]
-    ],
-    "success": true
+    ]
   }
 
 
@@ -499,10 +578,9 @@ Command line messages
 Reference implementation
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
-Ganeti's default iallocator is "hail" which is part of the separate
-ganeti-htools project. In order to see its source code please clone
-``git://git.ganeti.org/htools.git``. Note that htools is implemented
-using the Haskell programming language.
+Ganeti's default iallocator is "hail" which is available when "htools"
+components have been enabled at build time (see :doc:`install-quick` for
+more details).
 
 .. vim: set textwidth=72 :
 .. Local Variables:
index 415005c..34e57bb 100644 (file)
@@ -19,8 +19,12 @@ Contents:
    design-2.1.rst
    design-2.2.rst
    design-2.3.rst
+   design-htools-2.3.rst
    design-2.4.rst
+   design-draft.rst
+   design-network.rst
    cluster-merge.rst
+   design-shared-storage.rst
    locking.rst
    hooks.rst
    iallocator.rst
index 83659df..5d8b91a 100644 (file)
@@ -552,8 +552,8 @@ node, by using the ``--master-netdev <device>`` option.
 
 You can use a different name than ``xenvg`` for the volume group (but
 note that the name must be identical on all nodes). In this case you
-need to specify it by passing the *-g <VGNAME>* option to ``gnt-cluster
-init``.
+need to specify it by passing the *--vg-name <VGNAME>* option to
+``gnt-cluster init``.
 
 To set up the cluster as an Xen HVM cluster, use the
 ``--enabled-hypervisors=xen-hvm`` option to enable the HVM hypervisor
index 99f0590..f8853cb 100644 (file)
@@ -330,10 +330,23 @@ Redistribute configuration to all nodes. The result will be a job id.
 Returns a list of features supported by the RAPI server. Available
 features:
 
-``instance-create-reqv1``
+.. pyassert::
+
+  rlib2.ALL_FEATURES == set([rlib2._INST_CREATE_REQV1,
+                             rlib2._INST_REINSTALL_REQV1,
+                             rlib2._NODE_MIGRATE_REQV1,
+                             rlib2._NODE_EVAC_RES1])
+
+:pyeval:`rlib2._INST_CREATE_REQV1`
   Instance creation request data version 1 supported.
-``instance-reinstall-reqv1``
+:pyeval:`rlib2._INST_REINSTALL_REQV1`
   Instance reinstall supports body parameters.
+:pyeval:`rlib2._NODE_MIGRATE_REQV1`
+  Whether migrating a node (``/2/nodes/[node_name]/migrate``) supports
+  request body parameters.
+:pyeval:`rlib2._NODE_EVAC_RES1`
+  Whether evacuating a node (``/2/nodes/[node_name]/evacuate``) returns
+  a new-style result (see resource description)
 
 
 ``/2/modify``
@@ -350,53 +363,7 @@ Returns a job ID.
 
 Body parameters:
 
-``vg_name`` (string)
-  Volume group name.
-``enabled_hypervisors`` (list)
-  List of enabled hypervisors.
-``hvparams`` (dict)
-  Cluster-wide hypervisor parameter defaults, hypervisor-dependent.
-``beparams`` (dict)
-  Cluster-wide backend parameter defaults.
-``os_hvp`` (dict)
-  Cluster-wide per-OS hypervisor parameter defaults.
-``osparams`` (dict)
-  Dictionary with OS parameters.
-``candidate_pool_size`` (int)
-  Master candidate pool size.
-``uid_pool`` (list)
-  Set UID pool. Must be list of lists describing UID ranges (two items,
-  start and end inclusive).
-``add_uids``
-  Extend UID pool. Must be list of lists describing UID ranges (two
-  items, start and end inclusive) to be added.
-``remove_uids``
-  Shrink UID pool. Must be list of lists describing UID ranges (two
-  items, start and end inclusive) to be removed.
-``maintain_node_health`` (bool)
-  Whether to automatically maintain node health.
-``prealloc_wipe_disks`` (bool)
-  Whether to wipe disks before allocating them to instances.
-``nicparams`` (dict)
-  Cluster-wide NIC parameter defaults.
-``ndparams`` (dict)
-  Cluster-wide node parameter defaults.
-``drbd_helper`` (string)
-  DRBD helper program.
-``default_iallocator`` (string)
-  Default iallocator for cluster.
-``master_netdev`` (string)
-  Master network device.
-``reserved_lvs`` (list)
-  List of reserved LVs (strings).
-``hidden_os`` (list)
-  List of modifications as lists. Each modification must have two items,
-  the operation and the OS name. The operation can be ``add`` or
-  ``remove``.
-``blacklisted_os`` (list)
-  List of modifications as lists. Each modification must have two items,
-  the operation and the OS name. The operation can be ``add`` or
-  ``remove``.
+.. opcode_params:: OP_CLUSTER_SET_PARAMS
 
 
 ``/2/groups``
@@ -462,8 +429,10 @@ Returns: a job ID that can be used later for polling.
 
 Body parameters:
 
-``name`` (string, required)
-  Node group name.
+.. opcode_params:: OP_GROUP_ADD
+
+Earlier versions used a parameter named ``name`` which, while still
+supported, has been renamed to ``group_name``.
 
 
 ``/2/groups/[group_name]``
@@ -501,8 +470,8 @@ Returns a job ID.
 
 Body parameters:
 
-``alloc_policy`` (string)
-  If present, the new allocation policy for the node group.
+.. opcode_params:: OP_GROUP_SET_PARAMS
+   :exclude: group_name
 
 
 ``/2/groups/[group_name]/rename``
@@ -519,8 +488,8 @@ Returns a job ID.
 
 Body parameters:
 
-``new_name`` (string, required)
-  New node group name.
+.. opcode_params:: OP_GROUP_RENAME
+   :exclude: group_name
 
 
 ``/2/groups/[group_name]/assign-nodes``
@@ -537,8 +506,48 @@ Returns a job ID. It supports the ``dry-run`` and ``force`` arguments.
 
 Body parameters:
 
-``nodes`` (list, required)
-  One or more nodes to assign to the group.
+.. opcode_params:: OP_GROUP_ASSIGN_NODES
+   :exclude: group_name, force, dry_run
+
+
+``/2/groups/[group_name]/tags``
++++++++++++++++++++++++++++++++
+
+Manages per-nodegroup tags.
+
+Supports the following commands: ``GET``, ``PUT``, ``DELETE``.
+
+``GET``
+~~~~~~~
+
+Returns a list of tags.
+
+Example::
+
+    ["tag1", "tag2", "tag3"]
+
+``PUT``
+~~~~~~~
+
+Add a set of tags.
+
+The request as a list of strings should be ``PUT`` to this URI. The
+result will be a job id.
+
+It supports the ``dry-run`` argument.
+
+
+``DELETE``
+~~~~~~~~~~
+
+Delete a tag.
+
+In order to delete a set of tags, the DELETE request should be addressed
+to URI like::
+
+    /tags?tag=[tag]&tag=[tag]
+
+It supports the ``dry-run`` argument.
 
 
 ``/2/instances``
@@ -616,64 +625,14 @@ Body parameters:
 
 ``__version__`` (int, required)
   Must be ``1`` (older Ganeti versions used a different format for
-  instance creation requests, version ``0``, but that format is not
-  documented).
-``mode`` (string, required)
-  Instance creation mode.
-``name`` (string, required)
-  Instance name.
-``disk_template`` (string, required)
-  Disk template for instance.
-``disks`` (list, required)
-  List of disk definitions. Example: ``[{"size": 100}, {"size": 5}]``.
-  Each disk definition must contain a ``size`` value and can contain an
-  optional ``mode`` value denoting the disk access mode (``ro`` or
-  ``rw``).
-``nics`` (list, required)
-  List of NIC (network interface) definitions. Example: ``[{}, {},
-  {"ip": "198.51.100.4"}]``. Each NIC definition can contain the
-  optional values ``ip``, ``mode``, ``link`` and ``bridge``.
-``os`` (string, required)
-  Instance operating system.
-``osparams`` (dictionary)
-  Dictionary with OS parameters. If not valid for the given OS, the job
-  will fail.
-``force_variant`` (bool)
-  Whether to force an unknown variant.
-``no_install`` (bool)
-  Do not install the OS (will enable no-start)
-``pnode`` (string)
-  Primary node.
-``snode`` (string)
-  Secondary node.
-``src_node`` (string)
-  Source node for import.
-``src_path`` (string)
-  Source directory for import.
-``start`` (bool)
-  Whether to start instance after creation.
-``ip_check`` (bool)
-  Whether to ensure instance's IP address is inactive.
-``name_check`` (bool)
-  Whether to ensure instance's name is resolvable.
-``file_storage_dir`` (string)
-  File storage directory.
-``file_driver`` (string)
-  File storage driver.
-``iallocator`` (string)
-  Instance allocator name.
-``source_handshake`` (list)
-  Signed handshake from source (remote import only).
-``source_x509_ca`` (string)
-  Source X509 CA in PEM format (remote import only).
-``source_instance_name`` (string)
-  Source instance name (remote import only).
-``hypervisor`` (string)
-  Hypervisor name.
-``hvparams`` (dict)
-  Hypervisor parameters, hypervisor-dependent.
-``beparams`` (dict)
-  Backend parameters.
+  instance creation requests, version ``0``, but that format is no
+  longer supported)
+
+.. opcode_params:: OP_INSTANCE_CREATE
+
+Earlier versions used parameters named ``name`` and ``os``. These have
+been replaced by ``instance_name`` and ``os_type`` to match the
+underlying opcode. The old names can still be used.
 
 
 ``/2/instances/[instance_name]``
@@ -753,6 +712,9 @@ Shutdowns an instance.
 
 It supports the ``dry-run`` argument.
 
+.. opcode_params:: OP_INSTANCE_SHUTDOWN
+   :exclude: instance_name, dry_run
+
 
 ``/2/instances/[instance_name]/startup``
 ++++++++++++++++++++++++++++++++++++++++
@@ -860,10 +822,8 @@ Returns a job ID.
 
 Body parameters:
 
-``amount`` (int, required)
-  Amount of disk space to add.
-``wait_for_sync`` (bool)
-  Whether to wait for the disk to synchronize.
+.. opcode_params:: OP_INSTANCE_GROW_DISK
+   :exclude: instance_name, disk
 
 
 ``/2/instances/[instance_name]/prepare-export``
@@ -893,18 +853,9 @@ Returns a job ID.
 
 Body parameters:
 
-``mode`` (string)
-  Export mode.
-``destination`` (required)
-  Destination information, depends on export mode.
-``shutdown`` (bool, required)
-  Whether to shutdown instance before export.
-``remove_instance`` (bool)
-  Whether to remove instance after export.
-``x509_key_name``
-  Name of X509 key (remote export only).
-``destination_x509_ca``
-  Destination X509 CA (remote export only).
+.. opcode_params:: OP_BACKUP_EXPORT
+   :exclude: instance_name
+   :alias: target_node=destination
 
 
 ``/2/instances/[instance_name]/migrate``
@@ -921,10 +872,26 @@ Returns a job ID.
 
 Body parameters:
 
-``mode`` (string)
-  Migration mode.
-``cleanup`` (bool)
-  Whether a previously failed migration should be cleaned up.
+.. opcode_params:: OP_INSTANCE_MIGRATE
+   :exclude: instance_name, live
+
+
+``/2/instances/[instance_name]/failover``
++++++++++++++++++++++++++++++++++++++++++
+
+Does a failover of an instance.
+
+Supports the following commands: ``PUT``.
+
+``PUT``
+~~~~~~~
+
+Returns a job ID.
+
+Body parameters:
+
+.. opcode_params:: OP_INSTANCE_FAILOVER
+   :exclude: instance_name
 
 
 ``/2/instances/[instance_name]/rename``
@@ -941,12 +908,8 @@ Returns a job ID.
 
 Body parameters:
 
-``new_name`` (string, required)
-  New instance name.
-``ip_check`` (bool)
-  Whether to ensure instance's IP address is inactive.
-``name_check`` (bool)
-  Whether to ensure instance's name is resolvable.
+.. opcode_params:: OP_INSTANCE_RENAME
+   :exclude: instance_name
 
 
 ``/2/instances/[instance_name]/modify``
@@ -963,29 +926,8 @@ Returns a job ID.
 
 Body parameters:
 
-``osparams`` (dict)
-  Dictionary with OS parameters.
-``hvparams`` (dict)
-  Hypervisor parameters, hypervisor-dependent.
-``beparams`` (dict)
-  Backend parameters.
-``force`` (bool)
-  Whether to force the operation.
-``nics`` (list)
-  List of NIC changes. Each item is of the form ``(op, settings)``.
-  ``op`` can be ``add`` to add a new NIC with the specified settings,
-  ``remove`` to remove the last NIC or a number to modify the settings
-  of the NIC with that index.
-``disks`` (list)
-  List of disk changes. See ``nics``.
-``disk_template`` (string)
-  Disk template for instance.
-``remote_node`` (string)
-  Secondary node (used when changing disk template).
-``os_name`` (string)
-  Change instance's OS name. Does not reinstall the instance.
-``force_variant`` (bool)
-  Whether to force an unknown variant.
+.. opcode_params:: OP_INSTANCE_SET_PARAMS
+   :exclude: instance_name
 
 
 ``/2/instances/[instance_name]/console``
@@ -1114,42 +1056,49 @@ executing, so it's possible to retry the OpCode without side
 effects. But whether it make sense to retry depends on the error
 classification:
 
-``resolver_error``
+.. pyassert::
+
+   errors.ECODE_ALL == set([errors.ECODE_RESOLVER, errors.ECODE_NORES,
+     errors.ECODE_INVAL, errors.ECODE_STATE, errors.ECODE_NOENT,
+     errors.ECODE_EXISTS, errors.ECODE_NOTUNIQUE, errors.ECODE_FAULT,
+     errors.ECODE_ENVIRON])
+
+:pyeval:`errors.ECODE_RESOLVER`
   Resolver errors. This usually means that a name doesn't exist in DNS,
   so if it's a case of slow DNS propagation the operation can be retried
   later.
 
-``insufficient_resources``
+:pyeval:`errors.ECODE_NORES`
   Not enough resources (iallocator failure, disk space, memory,
   etc.). If the resources on the cluster increase, the operation might
   succeed.
 
-``wrong_input``
+:pyeval:`errors.ECODE_INVAL`
   Wrong arguments (at syntax level). The operation will not ever be
   accepted unless the arguments change.
 
-``wrong_state``
+:pyeval:`errors.ECODE_STATE`
   Wrong entity state. For example, live migration has been requested for
   a down instance, or instance creation on an offline node. The
   operation can be retried once the resource has changed state.
 
-``unknown_entity``
+:pyeval:`errors.ECODE_NOENT`
   Entity not found. For example, information has been requested for an
   unknown instance.
 
-``already_exists``
+:pyeval:`errors.ECODE_EXISTS`
   Entity already exists. For example, instance creation has been
   requested for an already-existing instance.
 
-``resource_not_unique``
+:pyeval:`errors.ECODE_NOTUNIQUE`
   Resource not unique (e.g. MAC or IP duplication).
 
-``internal_error``
+:pyeval:`errors.ECODE_FAULT`
   Internal cluster error. For example, a node is unreachable but not set
   offline, or the ganeti node daemons are not working, etc. A
   ``gnt-cluster verify`` should be run.
 
-``environment_error``
+:pyeval:`errors.ECODE_ENVIRON`
   Environment error (e.g. node disk error). A ``gnt-cluster verify``
   should be run.
 
@@ -1243,32 +1192,24 @@ It supports the following commands: ``GET``.
 ``/2/nodes/[node_name]/evacuate``
 +++++++++++++++++++++++++++++++++
 
-Evacuates all secondary instances off a node.
+Evacuates instances off a node.
 
 It supports the following commands: ``POST``.
 
 ``POST``
 ~~~~~~~~
 
-To evacuate a node, either one of the ``iallocator`` or ``remote_node``
-parameters must be passed::
-
-    evacuate?iallocator=[iallocator]
-    evacuate?remote_node=[nodeX.example.com]
+Returns a job ID. The result of the job will contain the IDs of the
+individual jobs submitted to evacuate the node.
 
-The result value will be a list, each element being a triple of the job
-id (for this specific evacuation), the instance which is being evacuated
-by this job, and the node to which it is being relocated. In case the
-node is already empty, the result will be an empty list (without any
-jobs being submitted).
+Body parameters:
 
-And additional parameter ``early_release`` signifies whether to try to
-parallelize the evacuations, at the risk of increasing I/O contention
-and increasing the chances of data loss, if the primary node of any of
-the instances being evacuated is not fully healthy.
+.. opcode_params:: OP_NODE_EVACUATE
+   :exclude: nodes
 
-If the dry-run parameter was specified, then the evacuation jobs were
-not actually submitted, and the job IDs will be null.
+Up to and including Ganeti 2.4 query arguments were used. Those are no
+longer supported. The new request can be detected by the presence of the
+:pyeval:`rlib2._NODE_EVAC_RES1` feature string.
 
 
 ``/2/nodes/[node_name]/migrate``
@@ -1282,13 +1223,14 @@ It supports the following commands: ``POST``.
 ~~~~~~~~
 
 If no mode is explicitly specified, each instances' hypervisor default
-migration mode will be used. Query parameters:
+migration mode will be used. Body parameters:
 
-``live`` (bool)
-  If set, use live migration if available.
-``mode`` (string)
-  Sets migration mode, ``live`` for live migration and ``non-live`` for
-  non-live migration. Supported by Ganeti 2.2 and above.
+.. opcode_params:: OP_NODE_MIGRATE
+   :exclude: node_name
+
+The query arguments used up to and including Ganeti 2.4 are deprecated
+and should no longer be used. The new request format can be detected by
+the presence of the :pyeval:`rlib2._NODE_MIGRATE_REQV1` feature string.
 
 
 ``/2/nodes/[node_name]/role``
@@ -1336,8 +1278,15 @@ Manages storage units on the node.
 ``GET``
 ~~~~~~~
 
+.. pyassert::
+
+   constants.VALID_STORAGE_TYPES == set([constants.ST_FILE,
+                                         constants.ST_LVM_PV,
+                                         constants.ST_LVM_VG])
+
 Requests a list of storage units on a node. Requires the parameters
-``storage_type`` (one of ``file``, ``lvm-pv`` or ``lvm-vg``) and
+``storage_type`` (one of :pyeval:`constants.ST_FILE`,
+:pyeval:`constants.ST_LVM_PV` or :pyeval:`constants.ST_LVM_VG`) and
 ``output_fields``. The result will be a job id, using which the result
 can be retrieved.
 
@@ -1350,10 +1299,11 @@ Modifies storage units on the node.
 ~~~~~~~
 
 Modifies parameters of storage units on the node. Requires the
-parameters ``storage_type`` (one of ``file``, ``lvm-pv`` or ``lvm-vg``)
+parameters ``storage_type`` (one of :pyeval:`constants.ST_FILE`,
+:pyeval:`constants.ST_LVM_PV` or :pyeval:`constants.ST_LVM_VG`)
 and ``name`` (name of the storage unit).  Parameters can be passed
-additionally. Currently only ``allocatable`` (bool) is supported. The
-result will be a job id.
+additionally. Currently only :pyeval:`constants.SF_ALLOCATABLE` (bool)
+is supported. The result will be a job id.
 
 ``/2/nodes/[node_name]/storage/repair``
 +++++++++++++++++++++++++++++++++++++++
@@ -1363,9 +1313,16 @@ Repairs a storage unit on the node.
 ``PUT``
 ~~~~~~~
 
+.. pyassert::
+
+   constants.VALID_STORAGE_OPERATIONS == {
+    constants.ST_LVM_VG: set([constants.SO_FIX_CONSISTENCY]),
+    }
+
 Repairs a storage unit on the node. Requires the parameters
-``storage_type`` (currently only ``lvm-vg`` can be repaired) and
-``name`` (name of the storage unit). The result will be a job id.
+``storage_type`` (currently only :pyeval:`constants.ST_LVM_VG` can be
+repaired) and ``name`` (name of the storage unit). The result will be a
+job id.
 
 ``/2/nodes/[node_name]/tags``
 +++++++++++++++++++++++++++++
@@ -1406,6 +1363,50 @@ to URI like::
 It supports the ``dry-run`` argument.
 
 
+``/2/query/[resource]``
++++++++++++++++++++++++
+
+Requests resource information. Available fields can be found in man
+pages and using ``/2/query/[resource]/fields``. The resource is one of
+:pyeval:`utils.CommaJoin(constants.QR_VIA_RAPI)`. See the :doc:`query2
+design document <design-query2>` for more details.
+
+Supports the following commands: ``GET``, ``PUT``.
+
+``GET``
+~~~~~~~
+
+Returns list of included fields and actual data. Takes a query parameter
+named "fields", containing a comma-separated list of field names. Does
+not support filtering.
+
+``PUT``
+~~~~~~~
+
+Returns list of included fields and actual data. The list of requested
+fields can either be given as the query parameter "fields" or as a body
+parameter with the same name. The optional body parameter "filter" can
+be given and must be either ``null`` or a list containing filter
+operators.
+
+
+``/2/query/[resource]/fields``
+++++++++++++++++++++++++++++++
+
+Request list of available fields for a resource. The resource is one of
+:pyeval:`utils.CommaJoin(constants.QR_VIA_RAPI)`. See the
+:doc:`query2 design document <design-query2>` for more details.
+
+Supports the following commands: ``GET``.
+
+``GET``
+~~~~~~~
+
+Returns a list of field descriptions for available fields. Takes an
+optional query parameter named "fields", containing a comma-separated
+list of field names.
+
+
 ``/2/os``
 +++++++++
 
index 06bdf6d..a173357 100644 (file)
@@ -592,8 +592,9 @@ And is now working again::
   node2   1.3T  1.3T  32.0G  1.0G 30.4G     1     3
   node3   1.3T  1.3T  32.0G  1.0G 30.4G     0     0
 
-.. note:: If you have the ganeti-htools package installed, you can
-   shuffle the instances around to have a better use of the nodes.
+.. note:: If you have the Ganeti has been built with the htools
+   component enabled, you can shuffle the instances around to have a
+   better use of the nodes.
 
 Disk failures
 +++++++++++++
@@ -788,7 +789,7 @@ Instance status
 
 As you can see, *instance4* has a copy running on node3, because we
 forced the failover when node3 failed. This case is dangerous as the
-instance will have the same IP and MAC address, wreaking havok on the
+instance will have the same IP and MAC address, wreaking havoc on the
 network environment and anyone who tries to use it.
 
 Ganeti doesn't directly handle this case. It is recommended to logon to
@@ -916,9 +917,9 @@ solve this, you have a number of options:
   for any non-trivial cluster)
 - try to reduce memory of some instances to accommodate the available
   node memory
-- if you have the ganeti-htools package installed, you can run the
-  ``hbal`` tool which will try to compute an automated cluster solution
-  that complies with the N+1 rule
+- if Ganeti has been built with the htools package enabled, you can run
+  the ``hbal`` tool which will try to compute an automated cluster
+  solution that complies with the N+1 rule
 
 Network issues
 ++++++++++++++
diff --git a/htools/Ganeti/Constants.hs.in b/htools/Ganeti/Constants.hs.in
new file mode 100644 (file)
index 0000000..369d001
--- /dev/null
@@ -0,0 +1,28 @@
+{-| Ganeti constants.
+
+These are duplicated from the Python code.
+
+-}
+
+{-
+
+Copyright (C) 2009, 2010, 2011 Google Inc.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+-}
+
+module Ganeti.Constants where
diff --git a/htools/Ganeti/HTools/CLI.hs b/htools/Ganeti/HTools/CLI.hs
new file mode 100644 (file)
index 0000000..d5fdc69
--- /dev/null
@@ -0,0 +1,524 @@
+{-| Implementation of command-line functions.
+
+This module holds the common command-line related functions for the
+binaries, separated into this module since "Ganeti.HTools.Utils" is
+used in many other places and this is more IO oriented.
+
+-}
+
+{-
+
+Copyright (C) 2009, 2010, 2011 Google Inc.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+-}
+
+module Ganeti.HTools.CLI
+    ( Options(..)
+    , OptType
+    , parseOpts
+    , shTemplate
+    , defaultLuxiSocket
+    , maybePrintNodes
+    , maybePrintInsts
+    , maybeShowWarnings
+    -- * The options
+    , oDataFile
+    , oDiskMoves
+    , oDiskTemplate
+    , oDynuFile
+    , oEvacMode
+    , oExInst
+    , oExTags
+    , oExecJobs
+    , oGroup
+    , oIDisk
+    , oIMem
+    , oIVcpus
+    , oInstMoves
+    , oLuxiSocket
+    , oMachineReadable
+    , oMaxCpu
+    , oMaxSolLength
+    , oMinDisk
+    , oMinGain
+    , oMinGainLim
+    , oMinScore
+    , oNoHeaders
+    , oNodeSim
+    , oOfflineNode
+    , oOneline
+    , oOutputDir
+    , oPrintCommands
+    , oPrintInsts
+    , oPrintNodes
+    , oQuiet
+    , oRapiMaster
+    , oReplay
+    , oSaveCluster
+    , oSelInst
+    , oShowHelp
+    , oShowVer
+    , oTieredSpec
+    , oVerbose
+    ) where
+
+import Control.Monad
+import Data.Maybe (fromMaybe)
+import qualified Data.Version
+import System.Console.GetOpt
+import System.IO
+import System.Info
+import System
+import Text.Printf (printf)
+
+import qualified Ganeti.HTools.Version as Version(version)
+import qualified Ganeti.Constants as C
+import Ganeti.HTools.Types
+import Ganeti.HTools.Utils
+
+-- * Constants
+
+-- | The default value for the luxi socket.
+--
+-- This is re-exported from the "Ganeti.Constants" module.
+defaultLuxiSocket :: FilePath
+defaultLuxiSocket = C.masterSocket
+
+-- * Data types
+
+-- | Command line options structure.
+data Options = Options
+    { optDataFile    :: Maybe FilePath -- ^ Path to the cluster data file
+    , optDiskMoves   :: Bool           -- ^ Allow disk moves
+    , optInstMoves   :: Bool           -- ^ Allow instance moves
+    , optDiskTemplate :: DiskTemplate  -- ^ The requested disk template
+    , optDynuFile    :: Maybe FilePath -- ^ Optional file with dynamic use data
+    , optEvacMode    :: Bool           -- ^ Enable evacuation mode
+    , optExInst      :: [String]       -- ^ Instances to be excluded
+    , optExTags      :: Maybe [String] -- ^ Tags to use for exclusion
+    , optExecJobs    :: Bool           -- ^ Execute the commands via Luxi
+    , optGroup       :: Maybe GroupID  -- ^ The UUID of the group to process
+    , optSelInst     :: [String]       -- ^ Instances to be excluded
+    , optISpec       :: RSpec          -- ^ Requested instance specs
+    , optLuxi        :: Maybe FilePath -- ^ Collect data from Luxi
+    , optMachineReadable :: Bool       -- ^ Output machine-readable format
+    , optMaster      :: String         -- ^ Collect data from RAPI
+    , optMaxLength   :: Int            -- ^ Stop after this many steps
+    , optMcpu        :: Double         -- ^ Max cpu ratio for nodes
+    , optMdsk        :: Double         -- ^ Max disk usage ratio for nodes
+    , optMinGain     :: Score          -- ^ Min gain we aim for in a step
+    , optMinGainLim  :: Score          -- ^ Limit below which we apply mingain
+    , optMinScore    :: Score          -- ^ The minimum score we aim for
+    , optNoHeaders   :: Bool           -- ^ Do not show a header line
+    , optNodeSim     :: [String]       -- ^ Cluster simulation mode
+    , optOffline     :: [String]       -- ^ Names of offline nodes
+    , optOneline     :: Bool           -- ^ Switch output to a single line
+    , optOutPath     :: FilePath       -- ^ Path to the output directory
+    , optSaveCluster :: Maybe FilePath -- ^ Save cluster state to this file
+    , optShowCmds    :: Maybe FilePath -- ^ Whether to show the command list
+    , optShowHelp    :: Bool           -- ^ Just show the help
+    , optShowInsts   :: Bool           -- ^ Whether to show the instance map
+    , optShowNodes   :: Maybe [String] -- ^ Whether to show node status
+    , optShowVer     :: Bool           -- ^ Just show the program version
+    , optTieredSpec  :: Maybe RSpec    -- ^ Requested specs for tiered mode
+    , optReplay      :: Maybe String   -- ^ Unittests: RNG state
+    , optVerbose     :: Int            -- ^ Verbosity level
+    } deriving Show
+
+-- | Default values for the command line options.
+defaultOptions :: Options
+defaultOptions  = Options
+ { optDataFile    = Nothing
+ , optDiskMoves   = True
+ , optInstMoves   = True
+ , optDiskTemplate = DTDrbd8
+ , optDynuFile    = Nothing
+ , optEvacMode    = False
+ , optExInst      = []
+ , optExTags      = Nothing
+ , optExecJobs    = False
+ , optGroup       = Nothing
+ , optSelInst     = []
+ , optISpec       = RSpec 1 4096 102400
+ , optLuxi        = Nothing
+ , optMachineReadable = False
+ , optMaster      = ""
+ , optMaxLength   = -1
+ , optMcpu        = defVcpuRatio
+ , optMdsk        = defReservedDiskRatio
+ , optMinGain     = 1e-2
+ , optMinGainLim  = 1e-1
+ , optMinScore    = 1e-9
+ , optNoHeaders   = False
+ , optNodeSim     = []
+ , optOffline     = []
+ , optOneline     = False
+ , optOutPath     = "."
+ , optSaveCluster = Nothing
+ , optShowCmds    = Nothing
+ , optShowHelp    = False
+ , optShowInsts   = False
+ , optShowNodes   = Nothing
+ , optShowVer     = False
+ , optTieredSpec  = Nothing
+ , optReplay      = Nothing
+ , optVerbose     = 1
+ }
+
+-- | Abrreviation for the option type.
+type OptType = OptDescr (Options -> Result Options)
+
+-- * Command line options
+
+oDataFile :: OptType
+oDataFile = Option "t" ["text-data"]
+            (ReqArg (\ f o -> Ok o { optDataFile = Just f }) "FILE")
+            "the cluster data FILE"
+
+oDiskMoves :: OptType
+oDiskMoves = Option "" ["no-disk-moves"]
+             (NoArg (\ opts -> Ok opts { optDiskMoves = False}))
+             "disallow disk moves from the list of allowed instance changes,\
+             \ thus allowing only the 'cheap' failover/migrate operations"
+
+oDiskTemplate :: OptType
+oDiskTemplate = Option "" ["disk-template"]
+                (ReqArg (\ t opts -> do
+                           dt <- dtFromString t
+                           return $ opts { optDiskTemplate = dt }) "TEMPLATE")
+                "select the desired disk template"
+
+oSelInst :: OptType
+oSelInst = Option "" ["select-instances"]
+          (ReqArg (\ f opts -> Ok opts { optSelInst = sepSplit ',' f }) "INSTS")
+          "only select given instances for any moves"
+
+oInstMoves :: OptType
+oInstMoves = Option "" ["no-instance-moves"]
+             (NoArg (\ opts -> Ok opts { optInstMoves = False}))
+             "disallow instance (primary node) moves from the list of allowed,\
+             \ instance changes, thus allowing only slower, but sometimes\
+             \ safer, drbd secondary changes"
+
+oDynuFile :: OptType
+oDynuFile = Option "U" ["dynu-file"]
+            (ReqArg (\ f opts -> Ok opts { optDynuFile = Just f }) "FILE")
+            "Import dynamic utilisation data from the given FILE"
+
+oEvacMode :: OptType
+oEvacMode = Option "E" ["evac-mode"]
+            (NoArg (\opts -> Ok opts { optEvacMode = True }))
+            "enable evacuation mode, where the algorithm only moves \
+            \ instances away from offline and drained nodes"
+
+oExInst :: OptType
+oExInst = Option "" ["exclude-instances"]
+          (ReqArg (\ f opts -> Ok opts { optExInst = sepSplit ',' f }) "INSTS")
+          "exclude given instances from any moves"
+
+oExTags :: OptType
+oExTags = Option "" ["exclusion-tags"]
+            (ReqArg (\ f opts -> Ok opts { optExTags = Just $ sepSplit ',' f })
+             "TAG,...") "Enable instance exclusion based on given tag prefix"
+
+oExecJobs :: OptType
+oExecJobs = Option "X" ["exec"]
+             (NoArg (\ opts -> Ok opts { optExecJobs = True}))
+             "execute the suggested moves via Luxi (only available when using\
+             \ it for data gathering)"
+
+oGroup :: OptType
+oGroup = Option "G" ["group"]
+            (ReqArg (\ f o -> Ok o { optGroup = Just f }) "ID")
+            "the ID of the group to balance"
+
+oIDisk :: OptType
+oIDisk = Option "" ["disk"]
+         (ReqArg (\ d opts -> do
+                    dsk <- annotateResult "--disk option" (parseUnit d)
+                    let ospec = optISpec opts
+                        nspec = ospec { rspecDsk = dsk }
+                    return $ opts { optISpec = nspec }) "DISK")
+         "disk size for instances"
+
+oIMem :: OptType
+oIMem = Option "" ["memory"]
+        (ReqArg (\ m opts -> do
+                   mem <- annotateResult "--memory option" (parseUnit m)
+                   let ospec = optISpec opts
+                       nspec = ospec { rspecMem = mem }
+                   return $ opts { optISpec = nspec }) "MEMORY")
+        "memory size for instances"
+
+oIVcpus :: OptType
+oIVcpus = Option "" ["vcpus"]
+          (ReqArg (\ p opts -> do
+                     vcpus <- tryRead "--vcpus option" p
+                     let ospec = optISpec opts
+                         nspec = ospec { rspecCpu = vcpus }
+                     return $ opts { optISpec = nspec }) "NUM")
+          "number of virtual cpus for instances"
+
+oLuxiSocket :: OptType
+oLuxiSocket = Option "L" ["luxi"]
+              (OptArg ((\ f opts -> Ok opts { optLuxi = Just f }) .
+                       fromMaybe defaultLuxiSocket) "SOCKET")
+              "collect data via Luxi, optionally using the given SOCKET path"
+
+oMachineReadable :: OptType
+oMachineReadable = Option "" ["machine-readable"]
+          (OptArg (\ f opts -> do
+                     flag <- parseYesNo True f
+                     return $ opts { optMachineReadable = flag }) "CHOICE")
+          "enable machine readable output (pass either 'yes' or 'no' to\
+          \ explicitely control the flag, or without an argument defaults to\
+          \ yes"
+
+oMaxCpu :: OptType
+oMaxCpu = Option "" ["max-cpu"]
+          (ReqArg (\ n opts -> Ok opts { optMcpu = read n }) "RATIO")
+          "maximum virtual-to-physical cpu ratio for nodes (from 1\
+          \ upwards) [64]"
+
+oMaxSolLength :: OptType
+oMaxSolLength = Option "l" ["max-length"]
+                (ReqArg (\ i opts -> Ok opts { optMaxLength = read i }) "N")
+                "cap the solution at this many moves (useful for very\
+                \ unbalanced clusters)"
+
+oMinDisk :: OptType
+oMinDisk = Option "" ["min-disk"]
+           (ReqArg (\ n opts -> Ok opts { optMdsk = read n }) "RATIO")
+           "minimum free disk space for nodes (between 0 and 1) [0]"
+
+oMinGain :: OptType
+oMinGain = Option "g" ["min-gain"]
+            (ReqArg (\ g opts -> Ok opts { optMinGain = read g }) "DELTA")
+            "minimum gain to aim for in a balancing step before giving up"
+
+oMinGainLim :: OptType
+oMinGainLim = Option "" ["min-gain-limit"]
+            (ReqArg (\ g opts -> Ok opts { optMinGainLim = read g }) "SCORE")
+            "minimum cluster score for which we start checking the min-gain"
+
+oMinScore :: OptType
+oMinScore = Option "e" ["min-score"]
+            (ReqArg (\ e opts -> Ok opts { optMinScore = read e }) "EPSILON")
+            "mininum score to aim for"
+
+oNoHeaders :: OptType
+oNoHeaders = Option "" ["no-headers"]
+             (NoArg (\ opts -> Ok opts { optNoHeaders = True }))
+             "do not show a header line"
+
+oNodeSim :: OptType
+oNodeSim = Option "" ["simulate"]
+            (ReqArg (\ f o -> Ok o { optNodeSim = f:optNodeSim o }) "SPEC")
+            "simulate an empty cluster, given as 'num_nodes,disk,ram,cpu'"
+
+oOfflineNode :: OptType
+oOfflineNode = Option "O" ["offline"]
+               (ReqArg (\ n o -> Ok o { optOffline = n:optOffline o }) "NODE")
+               "set node as offline"
+
+oOneline :: OptType
+oOneline = Option "o" ["oneline"]
+           (NoArg (\ opts -> Ok opts { optOneline = True }))
+           "print the ganeti command list for reaching the solution"
+
+oOutputDir :: OptType
+oOutputDir = Option "d" ["output-dir"]
+             (ReqArg (\ d opts -> Ok opts { optOutPath = d }) "PATH")
+             "directory in which to write output files"
+
+oPrintCommands :: OptType
+oPrintCommands = Option "C" ["print-commands"]
+                 (OptArg ((\ f opts -> Ok opts { optShowCmds = Just f }) .
+                          fromMaybe "-")
+                  "FILE")
+                 "print the ganeti command list for reaching the solution,\
+                 \ if an argument is passed then write the commands to a\
+                 \ file named as such"
+
+oPrintInsts :: OptType
+oPrintInsts = Option "" ["print-instances"]
+              (NoArg (\ opts -> Ok opts { optShowInsts = True }))
+              "print the final instance map"
+
+oPrintNodes :: OptType
+oPrintNodes = Option "p" ["print-nodes"]
+              (OptArg ((\ f opts ->
+                            let (prefix, realf) = case f of
+                                  '+':rest -> (["+"], rest)
+                                  _ -> ([], f)
+                                splitted = prefix ++ sepSplit ',' realf
+                            in Ok opts { optShowNodes = Just splitted }) .
+                       fromMaybe []) "FIELDS")
+              "print the final node list"
+
+oQuiet :: OptType
+oQuiet = Option "q" ["quiet"]
+         (NoArg (\ opts -> Ok opts { optVerbose = optVerbose opts - 1 }))
+         "decrease the verbosity level"
+
+oRapiMaster :: OptType
+oRapiMaster = Option "m" ["master"]
+              (ReqArg (\ m opts -> Ok opts { optMaster = m }) "ADDRESS")
+              "collect data via RAPI at the given ADDRESS"
+
+oSaveCluster :: OptType
+oSaveCluster = Option "S" ["save"]
+            (ReqArg (\ f opts -> Ok opts { optSaveCluster = Just f }) "FILE")
+            "Save cluster state at the end of the processing to FILE"
+
+oShowHelp :: OptType
+oShowHelp = Option "h" ["help"]
+            (NoArg (\ opts -> Ok opts { optShowHelp = True}))
+            "show help"
+
+oShowVer :: OptType
+oShowVer = Option "V" ["version"]
+           (NoArg (\ opts -> Ok opts { optShowVer = True}))
+           "show the version of the program"
+
+oTieredSpec :: OptType
+oTieredSpec = Option "" ["tiered-alloc"]
+             (ReqArg (\ inp opts -> do
+                          let sp = sepSplit ',' inp
+                          prs <- mapM (\(fn, val) -> fn val) $
+                                 zip [ annotateResult "tiered specs memory" .
+                                       parseUnit
+                                     , annotateResult "tiered specs disk" .
+                                       parseUnit
+                                     , tryRead "tiered specs cpus"
+                                     ] sp
+                          tspec <-
+                              case prs of
+                                [dsk, ram, cpu] -> return $ RSpec cpu ram dsk
+                                _ -> Bad $ "Invalid specification: " ++ inp ++
+                                     ", expected disk,ram,cpu"
+                          return $ opts { optTieredSpec = Just tspec } )
+              "TSPEC")
+             "enable tiered specs allocation, given as 'disk,ram,cpu'"
+
+oReplay :: OptType
+oReplay = Option "" ["replay"]
+          (ReqArg (\ stat opts -> Ok opts { optReplay = Just stat } ) "STATE")
+          "Pre-seed the random number generator with STATE"
+
+oVerbose :: OptType
+oVerbose = Option "v" ["verbose"]
+           (NoArg (\ opts -> Ok opts { optVerbose = optVerbose opts + 1 }))
+           "increase the verbosity level"
+
+-- * Functions
+
+-- | Helper for parsing a yes\/no command line flag.
+parseYesNo :: Bool         -- ^ Default whalue (when we get a @Nothing@)
+           -> Maybe String -- ^ Parameter value
+           -> Result Bool  -- ^ Resulting boolean value
+parseYesNo v Nothing      = return v
+parseYesNo _ (Just "yes") = return True
+parseYesNo _ (Just "no")  = return False
+parseYesNo _ (Just s)     = fail $ "Invalid choice '" ++ s ++
+                            "', pass one of 'yes' or 'no'"
+
+-- | Usage info.
+usageHelp :: String -> [OptType] -> String
+usageHelp progname =
+    usageInfo (printf "%s %s\nUsage: %s [OPTION...]"
+               progname Version.version progname)
+
+-- | Command line parser, using the 'Options' structure.
+parseOpts :: [String]               -- ^ The command line arguments
+          -> String                 -- ^ The program name
+          -> [OptType]              -- ^ The supported command line options
+          -> IO (Options, [String]) -- ^ The resulting options and leftover
+                                    -- arguments
+parseOpts argv progname options =
+    case getOpt Permute options argv of
+      (o, n, []) ->
+          do
+            let (pr, args) = (foldM (flip id) defaultOptions o, n)
+            po <- (case pr of
+                     Bad msg -> do
+                       hPutStrLn stderr "Error while parsing command\
+                                        \line arguments:"
+                       hPutStrLn stderr msg
+                       exitWith $ ExitFailure 1
+                     Ok val -> return val)
+            when (optShowHelp po) $ do
+              putStr $ usageHelp progname options
+              exitWith ExitSuccess
+            when (optShowVer po) $ do
+              printf "%s %s\ncompiled with %s %s\nrunning on %s %s\n"
+                     progname Version.version
+                     compilerName (Data.Version.showVersion compilerVersion)
+                     os arch :: IO ()
+              exitWith ExitSuccess
+            return (po, args)
+      (_, _, errs) -> do
+        hPutStrLn stderr $ "Command line error: "  ++ concat errs
+        hPutStrLn stderr $ usageHelp progname options
+        exitWith $ ExitFailure 2
+
+-- | A shell script template for autogenerated scripts.
+shTemplate :: String
+shTemplate =
+    printf "#!/bin/sh\n\n\
+           \# Auto-generated script for executing cluster rebalancing\n\n\
+           \# To stop, touch the file /tmp/stop-htools\n\n\
+           \set -e\n\n\
+           \check() {\n\
+           \  if [ -f /tmp/stop-htools ]; then\n\
+           \    echo 'Stop requested, exiting'\n\
+           \    exit 0\n\
+           \  fi\n\
+           \}\n\n"
+
+-- | Optionally print the node list.
+maybePrintNodes :: Maybe [String]       -- ^ The field list
+                -> String               -- ^ Informational message
+                -> ([String] -> String) -- ^ Function to generate the listing
+                -> IO ()
+maybePrintNodes Nothing _ _ = return ()
+maybePrintNodes (Just fields) msg fn = do
+  hPutStrLn stderr ""
+  hPutStrLn stderr (msg ++ " status:")
+  hPutStrLn stderr $ fn fields
+
+
+-- | Optionally print the instance list.
+maybePrintInsts :: Bool   -- ^ Whether to print the instance list
+                -> String -- ^ Type of the instance map (e.g. initial)
+                -> String -- ^ The instance data
+                -> IO ()
+maybePrintInsts do_print msg instdata =
+  when do_print $ do
+    hPutStrLn stderr ""
+    hPutStrLn stderr $ msg ++ " instance map:"
+    hPutStr stderr instdata
+
+-- | Function to display warning messages from parsing the cluster
+-- state.
+maybeShowWarnings :: [String] -- ^ The warning messages
+                  -> IO ()
+maybeShowWarnings fix_msgs =
+  unless (null fix_msgs) $ do
+    hPutStrLn stderr "Warning: cluster has inconsistent data:"
+    hPutStrLn stderr . unlines . map (printf "  - %s") $ fix_msgs
diff --git a/htools/Ganeti/HTools/Cluster.hs b/htools/Ganeti/HTools/Cluster.hs
new file mode 100644 (file)
index 0000000..9e94693
--- /dev/null
@@ -0,0 +1,1469 @@
+{-| Implementation of cluster-wide logic.
+
+This module holds all pure cluster-logic; I\/O related functionality
+goes into the /Main/ module for the individual binaries.
+
+-}
+
+{-
+
+Copyright (C) 2009, 2010, 2011 Google Inc.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+-}
+
+module Ganeti.HTools.Cluster
+    (
+     -- * Types
+      AllocSolution(..)
+    , EvacSolution(..)
+    , Table(..)
+    , CStats(..)
+    , AllocStats
+    -- * Generic functions
+    , totalResources
+    , computeAllocationDelta
+    -- * First phase functions
+    , computeBadItems
+    -- * Second phase functions
+    , printSolutionLine
+    , formatCmds
+    , involvedNodes
+    , splitJobs
+    -- * Display functions
+    , printNodes
+    , printInsts
+    -- * Balacing functions
+    , checkMove
+    , doNextBalance
+    , tryBalance
+    , compCV
+    , compCVNodes
+    , compDetailedCV
+    , printStats
+    , iMoveToJob
+    -- * IAllocator functions
+    , genAllocNodes
+    , tryAlloc
+    , tryMGAlloc
+    , tryReloc
+    , tryEvac
+    , tryNodeEvac
+    , tryChangeGroup
+    , collapseFailures
+    -- * Allocation functions
+    , iterateAlloc
+    , tieredAlloc
+     -- * Node group functions
+    , instanceGroup
+    , findSplitInstances
+    , splitCluster
+    ) where
+
+import qualified Data.IntSet as IntSet
+import Data.List
+import Data.Maybe (fromJust)
+import Data.Ord (comparing)
+import Text.Printf (printf)
+import Control.Monad
+
+import qualified Ganeti.HTools.Container as Container
+import qualified Ganeti.HTools.Instance as Instance
+import qualified Ganeti.HTools.Node as Node
+import qualified Ganeti.HTools.Group as Group
+import Ganeti.HTools.Types
+import Ganeti.HTools.Utils
+import Ganeti.HTools.Compat
+import qualified Ganeti.OpCodes as OpCodes
+
+-- * Types
+
+-- | Allocation\/relocation solution.
+data AllocSolution = AllocSolution
+  { asFailures  :: [FailMode]          -- ^ Failure counts
+  , asAllocs    :: Int                 -- ^ Good allocation count
+  , asSolutions :: [Node.AllocElement] -- ^ The actual result, length
+                                       -- of the list depends on the
+                                       -- allocation/relocation mode
+  , asLog       :: [String]            -- ^ A list of informational messages
+  }
+
+-- | Node evacuation/group change iallocator result type. This result
+-- type consists of actual opcodes (a restricted subset) that are
+-- transmitted back to Ganeti.
+data EvacSolution = EvacSolution
+    { esMoved   :: [(Idx, Gdx, [Ndx])]  -- ^ Instances moved successfully
+    , esFailed  :: [(Idx, String)]      -- ^ Instances which were not
+                                        -- relocated
+    , esOpCodes :: [[[OpCodes.OpCode]]] -- ^ List of lists of jobs
+    }
+
+-- | Allocation results, as used in 'iterateAlloc' and 'tieredAlloc'.
+type AllocResult = (FailStats, Node.List, Instance.List,
+                    [Instance.Instance], [CStats])
+
+-- | A type denoting the valid allocation mode/pairs.
+--
+-- For a one-node allocation, this will be a @Left ['Node.Node']@,
+-- whereas for a two-node allocation, this will be a @Right
+-- [('Node.Node', 'Node.Node')]@.
+type AllocNodes = Either [Ndx] [(Ndx, Ndx)]
+
+-- | The empty solution we start with when computing allocations.
+emptyAllocSolution :: AllocSolution
+emptyAllocSolution = AllocSolution { asFailures = [], asAllocs = 0
+                                   , asSolutions = [], asLog = [] }
+
+-- | The empty evac solution.
+emptyEvacSolution :: EvacSolution
+emptyEvacSolution = EvacSolution { esMoved = []
+                                 , esFailed = []
+                                 , esOpCodes = []
+                                 }
+
+-- | The complete state for the balancing solution.
+data Table = Table Node.List Instance.List Score [Placement]
+             deriving (Show, Read)
+
+-- | Cluster statistics data type.
+data CStats = CStats { csFmem :: Integer -- ^ Cluster free mem
+                     , csFdsk :: Integer -- ^ Cluster free disk
+                     , csAmem :: Integer -- ^ Cluster allocatable mem
+                     , csAdsk :: Integer -- ^ Cluster allocatable disk
+                     , csAcpu :: Integer -- ^ Cluster allocatable cpus
+                     , csMmem :: Integer -- ^ Max node allocatable mem
+                     , csMdsk :: Integer -- ^ Max node allocatable disk
+                     , csMcpu :: Integer -- ^ Max node allocatable cpu
+                     , csImem :: Integer -- ^ Instance used mem
+                     , csIdsk :: Integer -- ^ Instance used disk
+                     , csIcpu :: Integer -- ^ Instance used cpu
+                     , csTmem :: Double  -- ^ Cluster total mem
+                     , csTdsk :: Double  -- ^ Cluster total disk
+                     , csTcpu :: Double  -- ^ Cluster total cpus
+                     , csVcpu :: Integer -- ^ Cluster virtual cpus (if
+                                         -- node pCpu has been set,
+                                         -- otherwise -1)
+                     , csXmem :: Integer -- ^ Unnacounted for mem
+                     , csNmem :: Integer -- ^ Node own memory
+                     , csScore :: Score  -- ^ The cluster score
+                     , csNinst :: Int    -- ^ The total number of instances
+                     }
+            deriving (Show, Read)
+
+-- | Currently used, possibly to allocate, unallocable.
+type AllocStats = (RSpec, RSpec, RSpec)
+
+-- * Utility functions
+
+-- | Verifies the N+1 status and return the affected nodes.
+verifyN1 :: [Node.Node] -> [Node.Node]
+verifyN1 = filter Node.failN1
+
+{-| Computes the pair of bad nodes and instances.
+
+The bad node list is computed via a simple 'verifyN1' check, and the
+bad instance list is the list of primary and secondary instances of
+those nodes.
+
+-}
+computeBadItems :: Node.List -> Instance.List ->
+                   ([Node.Node], [Instance.Instance])
+computeBadItems nl il =
+  let bad_nodes = verifyN1 $ getOnline nl
+      bad_instances = map (`Container.find` il) .
+                      sort . nub $
+                      concatMap (\ n -> Node.sList n ++ Node.pList n) bad_nodes
+  in
+    (bad_nodes, bad_instances)
+
+-- | Zero-initializer for the CStats type.
+emptyCStats :: CStats
+emptyCStats = CStats 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
+
+-- | Update stats with data from a new node.
+updateCStats :: CStats -> Node.Node -> CStats
+updateCStats cs node =
+    let CStats { csFmem = x_fmem, csFdsk = x_fdsk,
+                 csAmem = x_amem, csAcpu = x_acpu, csAdsk = x_adsk,
+                 csMmem = x_mmem, csMdsk = x_mdsk, csMcpu = x_mcpu,
+                 csImem = x_imem, csIdsk = x_idsk, csIcpu = x_icpu,
+                 csTmem = x_tmem, csTdsk = x_tdsk, csTcpu = x_tcpu,
+                 csVcpu = x_vcpu,
+                 csXmem = x_xmem, csNmem = x_nmem, csNinst = x_ninst
+               }
+            = cs
+        inc_amem = Node.fMem node - Node.rMem node
+        inc_amem' = if inc_amem > 0 then inc_amem else 0
+        inc_adsk = Node.availDisk node
+        inc_imem = truncate (Node.tMem node) - Node.nMem node
+                   - Node.xMem node - Node.fMem node
+        inc_icpu = Node.uCpu node
+        inc_idsk = truncate (Node.tDsk node) - Node.fDsk node
+        inc_vcpu = Node.hiCpu node
+        inc_acpu = Node.availCpu node
+
+    in cs { csFmem = x_fmem + fromIntegral (Node.fMem node)
+          , csFdsk = x_fdsk + fromIntegral (Node.fDsk node)
+          , csAmem = x_amem + fromIntegral inc_amem'
+          , csAdsk = x_adsk + fromIntegral inc_adsk
+          , csAcpu = x_acpu + fromIntegral inc_acpu
+          , csMmem = max x_mmem (fromIntegral inc_amem')
+          , csMdsk = max x_mdsk (fromIntegral inc_adsk)
+          , csMcpu = max x_mcpu (fromIntegral inc_acpu)
+          , csImem = x_imem + fromIntegral inc_imem
+          , csIdsk = x_idsk + fromIntegral inc_idsk
+          , csIcpu = x_icpu + fromIntegral inc_icpu
+          , csTmem = x_tmem + Node.tMem node
+          , csTdsk = x_tdsk + Node.tDsk node
+          , csTcpu = x_tcpu + Node.tCpu node
+          , csVcpu = x_vcpu + fromIntegral inc_vcpu
+          , csXmem = x_xmem + fromIntegral (Node.xMem node)
+          , csNmem = x_nmem + fromIntegral (Node.nMem node)
+          , csNinst = x_ninst + length (Node.pList node)
+          }
+
+-- | Compute the total free disk and memory in the cluster.
+totalResources :: Node.List -> CStats
+totalResources nl =
+    let cs = foldl' updateCStats emptyCStats . Container.elems $ nl
+    in cs { csScore = compCV nl }
+
+-- | Compute the delta between two cluster state.
+--
+-- This is used when doing allocations, to understand better the
+-- available cluster resources. The return value is a triple of the
+-- current used values, the delta that was still allocated, and what
+-- was left unallocated.
+computeAllocationDelta :: CStats -> CStats -> AllocStats
+computeAllocationDelta cini cfin =
+    let CStats {csImem = i_imem, csIdsk = i_idsk, csIcpu = i_icpu} = cini
+        CStats {csImem = f_imem, csIdsk = f_idsk, csIcpu = f_icpu,
+                csTmem = t_mem, csTdsk = t_dsk, csVcpu = v_cpu } = cfin
+        rini = RSpec (fromIntegral i_icpu) (fromIntegral i_imem)
+               (fromIntegral i_idsk)
+        rfin = RSpec (fromIntegral (f_icpu - i_icpu))
+               (fromIntegral (f_imem - i_imem))
+               (fromIntegral (f_idsk - i_idsk))
+        un_cpu = fromIntegral (v_cpu - f_icpu)::Int
+        runa = RSpec un_cpu (truncate t_mem - fromIntegral f_imem)
+               (truncate t_dsk - fromIntegral f_idsk)
+    in (rini, rfin, runa)
+
+-- | The names and weights of the individual elements in the CV list.
+detailedCVInfo :: [(Double, String)]
+detailedCVInfo = [ (1,  "free_mem_cv")
+                 , (1,  "free_disk_cv")
+                 , (1,  "n1_cnt")
+                 , (1,  "reserved_mem_cv")
+                 , (4,  "offline_all_cnt")
+                 , (16, "offline_pri_cnt")
+                 , (1,  "vcpu_ratio_cv")
+                 , (1,  "cpu_load_cv")
+                 , (1,  "mem_load_cv")
+                 , (1,  "disk_load_cv")
+                 , (1,  "net_load_cv")
+                 , (2,  "pri_tags_score")
+                 ]
+
+-- | Holds the weights used by 'compCVNodes' for each metric.
+detailedCVWeights :: [Double]
+detailedCVWeights = map fst detailedCVInfo
+
+-- | Compute the mem and disk covariance.
+compDetailedCV :: [Node.Node] -> [Double]
+compDetailedCV all_nodes =
+    let
+        (offline, nodes) = partition Node.offline all_nodes
+        mem_l = map Node.pMem nodes
+        dsk_l = map Node.pDsk nodes
+        -- metric: memory covariance
+        mem_cv = stdDev mem_l
+        -- metric: disk covariance
+        dsk_cv = stdDev dsk_l
+        -- metric: count of instances living on N1 failing nodes
+        n1_score = fromIntegral . sum . map (\n -> length (Node.sList n) +
+                                                   length (Node.pList n)) .
+                   filter Node.failN1 $ nodes :: Double
+        res_l = map Node.pRem nodes
+        -- metric: reserved memory covariance
+        res_cv = stdDev res_l
+        -- offline instances metrics
+        offline_ipri = sum . map (length . Node.pList) $ offline
+        offline_isec = sum . map (length . Node.sList) $ offline
+        -- metric: count of instances on offline nodes
+        off_score = fromIntegral (offline_ipri + offline_isec)::Double
+        -- metric: count of primary instances on offline nodes (this
+        -- helps with evacuation/failover of primary instances on
+        -- 2-node clusters with one node offline)
+        off_pri_score = fromIntegral offline_ipri::Double
+        cpu_l = map Node.pCpu nodes
+        -- metric: covariance of vcpu/pcpu ratio
+        cpu_cv = stdDev cpu_l
+        -- metrics: covariance of cpu, memory, disk and network load
+        (c_load, m_load, d_load, n_load) = unzip4 $
+            map (\n ->
+                     let DynUtil c1 m1 d1 n1 = Node.utilLoad n
+                         DynUtil c2 m2 d2 n2 = Node.utilPool n
+                     in (c1/c2, m1/m2, d1/d2, n1/n2)
+                ) nodes
+        -- metric: conflicting instance count
+        pri_tags_inst = sum $ map Node.conflictingPrimaries nodes
+        pri_tags_score = fromIntegral pri_tags_inst::Double
+    in [ mem_cv, dsk_cv, n1_score, res_cv, off_score, off_pri_score, cpu_cv
+       , stdDev c_load, stdDev m_load , stdDev d_load, stdDev n_load
+       , pri_tags_score ]
+
+-- | Compute the /total/ variance.
+compCVNodes :: [Node.Node] -> Double
+compCVNodes = sum . zipWith (*) detailedCVWeights . compDetailedCV
+
+-- | Wrapper over 'compCVNodes' for callers that have a 'Node.List'.
+compCV :: Node.List -> Double
+compCV = compCVNodes . Container.elems
+
+-- | Compute online nodes from a 'Node.List'.
+getOnline :: Node.List -> [Node.Node]
+getOnline = filter (not . Node.offline) . Container.elems
+
+-- * Balancing functions
+
+-- | Compute best table. Note that the ordering of the arguments is important.
+compareTables :: Table -> Table -> Table
+compareTables a@(Table _ _ a_cv _) b@(Table _ _ b_cv _ ) =
+    if a_cv > b_cv then b else a
+
+-- | Applies an instance move to a given node list and instance.
+applyMove :: Node.List -> Instance.Instance
+          -> IMove -> OpResult (Node.List, Instance.Instance, Ndx, Ndx)
+-- Failover (f)
+applyMove nl inst Failover =
+    let old_pdx = Instance.pNode inst
+        old_sdx = Instance.sNode inst
+        old_p = Container.find old_pdx nl
+        old_s = Container.find old_sdx nl
+        int_p = Node.removePri old_p inst
+        int_s = Node.removeSec old_s inst
+        force_p = Node.offline old_p
+        new_nl = do -- Maybe monad
+          new_p <- Node.addPriEx force_p int_s inst
+          new_s <- Node.addSec int_p inst old_sdx
+          let new_inst = Instance.setBoth inst old_sdx old_pdx
+          return (Container.addTwo old_pdx new_s old_sdx new_p nl,
+                  new_inst, old_sdx, old_pdx)
+    in new_nl
+
+-- Replace the primary (f:, r:np, f)
+applyMove nl inst (ReplacePrimary new_pdx) =
+    let old_pdx = Instance.pNode inst
+        old_sdx = Instance.sNode inst
+        old_p = Container.find old_pdx nl
+        old_s = Container.find old_sdx nl
+        tgt_n = Container.find new_pdx nl
+        int_p = Node.removePri old_p inst
+        int_s = Node.removeSec old_s inst
+        force_p = Node.offline old_p
+        new_nl = do -- Maybe monad
+          -- check that the current secondary can host the instance
+          -- during the migration
+          tmp_s <- Node.addPriEx force_p int_s inst
+          let tmp_s' = Node.removePri tmp_s inst
+          new_p <- Node.addPriEx force_p tgt_n inst
+          new_s <- Node.addSecEx force_p tmp_s' inst new_pdx
+          let new_inst = Instance.setPri inst new_pdx
+          return (Container.add new_pdx new_p $
+                  Container.addTwo old_pdx int_p old_sdx new_s nl,
+                  new_inst, new_pdx, old_sdx)
+    in new_nl
+
+-- Replace the secondary (r:ns)
+applyMove nl inst (ReplaceSecondary new_sdx) =
+    let old_pdx = Instance.pNode inst
+        old_sdx = Instance.sNode inst
+        old_s = Container.find old_sdx nl
+        tgt_n = Container.find new_sdx nl
+        int_s = Node.removeSec old_s inst
+        force_s = Node.offline old_s
+        new_inst = Instance.setSec inst new_sdx
+        new_nl = Node.addSecEx force_s tgt_n inst old_pdx >>=
+                 \new_s -> return (Container.addTwo new_sdx
+                                   new_s old_sdx int_s nl,
+                                   new_inst, old_pdx, new_sdx)
+    in new_nl
+
+-- Replace the secondary and failover (r:np, f)
+applyMove nl inst (ReplaceAndFailover new_pdx) =
+    let old_pdx = Instance.pNode inst
+        old_sdx = Instance.sNode inst
+        old_p = Container.find old_pdx nl
+        old_s = Container.find old_sdx nl
+        tgt_n = Container.find new_pdx nl
+        int_p = Node.removePri old_p inst
+        int_s = Node.removeSec old_s inst
+        force_s = Node.offline old_s
+        new_nl = do -- Maybe monad
+          new_p <- Node.addPri tgt_n inst
+          new_s <- Node.addSecEx force_s int_p inst new_pdx
+          let new_inst = Instance.setBoth inst new_pdx old_pdx
+          return (Container.add new_pdx new_p $
+                  Container.addTwo old_pdx new_s old_sdx int_s nl,
+                  new_inst, new_pdx, old_pdx)
+    in new_nl
+
+-- Failver and replace the secondary (f, r:ns)
+applyMove nl inst (FailoverAndReplace new_sdx) =
+    let old_pdx = Instance.pNode inst
+        old_sdx = Instance.sNode inst
+        old_p = Container.find old_pdx nl
+        old_s = Container.find old_sdx nl
+        tgt_n = Container.find new_sdx nl
+        int_p = Node.removePri old_p inst
+        int_s = Node.removeSec old_s inst
+        force_p = Node.offline old_p
+        new_nl = do -- Maybe monad
+          new_p <- Node.addPriEx force_p int_s inst
+          new_s <- Node.addSecEx force_p tgt_n inst old_sdx
+          let new_inst = Instance.setBoth inst old_sdx new_sdx
+          return (Container.add new_sdx new_s $
+                  Container.addTwo old_sdx new_p old_pdx int_p nl,
+                  new_inst, old_sdx, new_sdx)
+    in new_nl
+
+-- | Tries to allocate an instance on one given node.
+allocateOnSingle :: Node.List -> Instance.Instance -> Ndx
+                 -> OpResult Node.AllocElement
+allocateOnSingle nl inst new_pdx =
+    let p = Container.find new_pdx nl
+        new_inst = Instance.setBoth inst new_pdx Node.noSecondary
+    in  Node.addPri p inst >>= \new_p -> do
+      let new_nl = Container.add new_pdx new_p nl
+          new_score = compCV nl
+      return (new_nl, new_inst, [new_p], new_score)
+
+-- | Tries to allocate an instance on a given pair of nodes.
+allocateOnPair :: Node.List -> Instance.Instance -> Ndx -> Ndx
+               -> OpResult Node.AllocElement
+allocateOnPair nl inst new_pdx new_sdx =
+    let tgt_p = Container.find new_pdx nl
+        tgt_s = Container.find new_sdx nl
+    in do
+      new_p <- Node.addPri tgt_p inst
+      new_s <- Node.addSec tgt_s inst new_pdx
+      let new_inst = Instance.setBoth inst new_pdx new_sdx
+          new_nl = Container.addTwo new_pdx new_p new_sdx new_s nl
+      return (new_nl, new_inst, [new_p, new_s], compCV new_nl)
+
+-- | Tries to perform an instance move and returns the best table
+-- between the original one and the new one.
+checkSingleStep :: Table -- ^ The original table
+                -> Instance.Instance -- ^ The instance to move
+                -> Table -- ^ The current best table
+                -> IMove -- ^ The move to apply
+                -> Table -- ^ The final best table
+checkSingleStep ini_tbl target cur_tbl move =
+    let
+        Table ini_nl ini_il _ ini_plc = ini_tbl
+        tmp_resu = applyMove ini_nl target move
+    in
+      case tmp_resu of
+        OpFail _ -> cur_tbl
+        OpGood (upd_nl, new_inst, pri_idx, sec_idx) ->
+            let tgt_idx = Instance.idx target
+                upd_cvar = compCV upd_nl
+                upd_il = Container.add tgt_idx new_inst ini_il
+                upd_plc = (tgt_idx, pri_idx, sec_idx, move, upd_cvar):ini_plc
+                upd_tbl = Table upd_nl upd_il upd_cvar upd_plc
+            in
+              compareTables cur_tbl upd_tbl
+
+-- | Given the status of the current secondary as a valid new node and
+-- the current candidate target node, generate the possible moves for
+-- a instance.
+possibleMoves :: Bool      -- ^ Whether the secondary node is a valid new node
+              -> Bool      -- ^ Whether we can change the primary node
+              -> Ndx       -- ^ Target node candidate
+              -> [IMove]   -- ^ List of valid result moves
+
+possibleMoves _ False tdx =
+    [ReplaceSecondary tdx]
+
+possibleMoves True True tdx =
+    [ReplaceSecondary tdx,
+     ReplaceAndFailover tdx,
+     ReplacePrimary tdx,
+     FailoverAndReplace tdx]
+
+possibleMoves False True tdx =
+    [ReplaceSecondary tdx,
+     ReplaceAndFailover tdx]
+
+-- | Compute the best move for a given instance.
+checkInstanceMove :: [Ndx]             -- ^ Allowed target node indices
+                  -> Bool              -- ^ Whether disk moves are allowed
+                  -> Bool              -- ^ Whether instance moves are allowed
+                  -> Table             -- ^ Original table
+                  -> Instance.Instance -- ^ Instance to move
+                  -> Table             -- ^ Best new table for this instance
+checkInstanceMove nodes_idx disk_moves inst_moves ini_tbl target =
+    let
+        opdx = Instance.pNode target
+        osdx = Instance.sNode target
+        nodes = filter (\idx -> idx /= opdx && idx /= osdx) nodes_idx
+        use_secondary = elem osdx nodes_idx && inst_moves
+        aft_failover = if use_secondary -- if allowed to failover
+                       then checkSingleStep ini_tbl target ini_tbl Failover
+                       else ini_tbl
+        all_moves = if disk_moves
+                    then concatMap
+                         (possibleMoves use_secondary inst_moves) nodes
+                    else []
+    in
+      -- iterate over the possible nodes for this instance
+      foldl' (checkSingleStep ini_tbl target) aft_failover all_moves
+
+-- | Compute the best next move.
+checkMove :: [Ndx]               -- ^ Allowed target node indices
+          -> Bool                -- ^ Whether disk moves are allowed
+          -> Bool                -- ^ Whether instance moves are allowed
+          -> Table               -- ^ The current solution
+          -> [Instance.Instance] -- ^ List of instances still to move
+          -> Table               -- ^ The new solution
+checkMove nodes_idx disk_moves inst_moves ini_tbl victims =
+    let Table _ _ _ ini_plc = ini_tbl
+        -- we're using rwhnf from the Control.Parallel.Strategies
+        -- package; we don't need to use rnf as that would force too
+        -- much evaluation in single-threaded cases, and in
+        -- multi-threaded case the weak head normal form is enough to
+        -- spark the evaluation
+        tables = parMap rwhnf (checkInstanceMove nodes_idx disk_moves
+                               inst_moves ini_tbl)
+                 victims
+        -- iterate over all instances, computing the best move
+        best_tbl = foldl' compareTables ini_tbl tables
+        Table _ _ _ best_plc = best_tbl
+    in if length best_plc == length ini_plc
+       then ini_tbl -- no advancement
+       else best_tbl
+
+-- | Check if we are allowed to go deeper in the balancing.
+doNextBalance :: Table     -- ^ The starting table
+              -> Int       -- ^ Remaining length
+              -> Score     -- ^ Score at which to stop
+              -> Bool      -- ^ The resulting table and commands
+doNextBalance ini_tbl max_rounds min_score =
+    let Table _ _ ini_cv ini_plc = ini_tbl
+        ini_plc_len = length ini_plc
+    in (max_rounds < 0 || ini_plc_len < max_rounds) && ini_cv > min_score
+
+-- | Run a balance move.
+tryBalance :: Table       -- ^ The starting table
+           -> Bool        -- ^ Allow disk moves
+           -> Bool        -- ^ Allow instance moves
+           -> Bool        -- ^ Only evacuate moves
+           -> Score       -- ^ Min gain threshold
+           -> Score       -- ^ Min gain
+           -> Maybe Table -- ^ The resulting table and commands
+tryBalance ini_tbl disk_moves inst_moves evac_mode mg_limit min_gain =
+    let Table ini_nl ini_il ini_cv _ = ini_tbl
+        all_inst = Container.elems ini_il
+        all_inst' = if evac_mode
+                    then let bad_nodes = map Node.idx . filter Node.offline $
+                                         Container.elems ini_nl
+                         in filter (any (`elem` bad_nodes) . Instance.allNodes)
+                            all_inst
+                    else all_inst
+        reloc_inst = filter Instance.movable all_inst'
+        node_idx = map Node.idx . filter (not . Node.offline) $
+                   Container.elems ini_nl
+        fin_tbl = checkMove node_idx disk_moves inst_moves ini_tbl reloc_inst
+        (Table _ _ fin_cv _) = fin_tbl
+    in
+      if fin_cv < ini_cv && (ini_cv > mg_limit || ini_cv - fin_cv >= min_gain)
+      then Just fin_tbl -- this round made success, return the new table
+      else Nothing
+
+-- * Allocation functions
+
+-- | Build failure stats out of a list of failures.
+collapseFailures :: [FailMode] -> FailStats
+collapseFailures flst =
+    map (\k -> (k, foldl' (\a e -> if e == k then a + 1 else a) 0 flst))
+            [minBound..maxBound]
+
+-- | Update current Allocation solution and failure stats with new
+-- elements.
+concatAllocs :: AllocSolution -> OpResult Node.AllocElement -> AllocSolution
+concatAllocs as (OpFail reason) = as { asFailures = reason : asFailures as }
+
+concatAllocs as (OpGood ns@(_, _, _, nscore)) =
+    let -- Choose the old or new solution, based on the cluster score
+        cntok = asAllocs as
+        osols = asSolutions as
+        nsols = case osols of
+                  [] -> [ns]
+                  (_, _, _, oscore):[] ->
+                      if oscore < nscore
+                      then osols
+                      else [ns]
+                  -- FIXME: here we simply concat to lists with more
+                  -- than one element; we should instead abort, since
+                  -- this is not a valid usage of this function
+                  xs -> ns:xs
+        nsuc = cntok + 1
+    -- Note: we force evaluation of nsols here in order to keep the
+    -- memory profile low - we know that we will need nsols for sure
+    -- in the next cycle, so we force evaluation of nsols, since the
+    -- foldl' in the caller will only evaluate the tuple, but not the
+    -- elements of the tuple
+    in nsols `seq` nsuc `seq` as { asAllocs = nsuc, asSolutions = nsols }
+
+-- | Given a solution, generates a reasonable description for it.
+describeSolution :: AllocSolution -> String
+describeSolution as =
+  let fcnt = asFailures as
+      sols = asSolutions as
+      freasons =
+        intercalate ", " . map (\(a, b) -> printf "%s: %d" (show a) b) .
+        filter ((> 0) . snd) . collapseFailures $ fcnt
+  in if null sols
+     then "No valid allocation solutions, failure reasons: " ++
+          (if null fcnt
+           then "unknown reasons"
+           else freasons)
+     else let (_, _, nodes, cv) = head sols
+          in printf ("score: %.8f, successes %d, failures %d (%s)" ++
+                     " for node(s) %s") cv (asAllocs as) (length fcnt) freasons
+             (intercalate "/" . map Node.name $ nodes)
+
+-- | Annotates a solution with the appropriate string.
+annotateSolution :: AllocSolution -> AllocSolution
+annotateSolution as = as { asLog = describeSolution as : asLog as }
+
+-- | Reverses an evacuation solution.
+--
+-- Rationale: we always concat the results to the top of the lists, so
+-- for proper jobset execution, we should reverse all lists.
+reverseEvacSolution :: EvacSolution -> EvacSolution
+reverseEvacSolution (EvacSolution f m o) =
+    EvacSolution (reverse f) (reverse m) (reverse o)
+
+-- | Generate the valid node allocation singles or pairs for a new instance.
+genAllocNodes :: Group.List        -- ^ Group list
+              -> Node.List         -- ^ The node map
+              -> Int               -- ^ The number of nodes required
+              -> Bool              -- ^ Whether to drop or not
+                                   -- unallocable nodes
+              -> Result AllocNodes -- ^ The (monadic) result
+genAllocNodes gl nl count drop_unalloc =
+    let filter_fn = if drop_unalloc
+                    then filter (Group.isAllocable .
+                                 flip Container.find gl . Node.group)
+                    else id
+        all_nodes = filter_fn $ getOnline nl
+        all_pairs = liftM2 (,) all_nodes all_nodes
+        ok_pairs = filter (\(x, y) -> Node.idx x /= Node.idx y &&
+                                      Node.group x == Node.group y) all_pairs
+    in case count of
+         1 -> Ok (Left (map Node.idx all_nodes))
+         2 -> Ok (Right (map (\(p, s) -> (Node.idx p, Node.idx s)) ok_pairs))
+         _ -> Bad "Unsupported number of nodes, only one or two  supported"
+
+-- | Try to allocate an instance on the cluster.
+tryAlloc :: (Monad m) =>
+            Node.List         -- ^ The node list
+         -> Instance.List     -- ^ The instance list
+         -> Instance.Instance -- ^ The instance to allocate
+         -> AllocNodes        -- ^ The allocation targets
+         -> m AllocSolution   -- ^ Possible solution list
+tryAlloc nl _ inst (Right ok_pairs) =
+    let sols = foldl' (\cstate (p, s) ->
+                           concatAllocs cstate $ allocateOnPair nl inst p s
+                      ) emptyAllocSolution ok_pairs
+
+    in if null ok_pairs -- means we have just one node
+       then fail "Not enough online nodes"
+       else return $ annotateSolution sols
+
+tryAlloc nl _ inst (Left all_nodes) =
+    let sols = foldl' (\cstate ->
+                           concatAllocs cstate . allocateOnSingle nl inst
+                      ) emptyAllocSolution all_nodes
+    in if null all_nodes
+       then fail "No online nodes"
+       else return $ annotateSolution sols
+
+-- | Given a group/result, describe it as a nice (list of) messages.
+solutionDescription :: Group.List -> (Gdx, Result AllocSolution) -> [String]
+solutionDescription gl (groupId, result) =
+  case result of
+    Ok solution -> map (printf "Group %s (%s): %s" gname pol) (asLog solution)
+    Bad message -> [printf "Group %s: error %s" gname message]
+  where grp = Container.find groupId gl
+        gname = Group.name grp
+        pol = apolToString (Group.allocPolicy grp)
+
+-- | From a list of possibly bad and possibly empty solutions, filter
+-- only the groups with a valid result. Note that the result will be
+-- reversed compared to the original list.
+filterMGResults :: Group.List
+                -> [(Gdx, Result AllocSolution)]
+                -> [(Gdx, AllocSolution)]
+filterMGResults gl = foldl' fn []
+    where unallocable = not . Group.isAllocable . flip Container.find gl
+          fn accu (gdx, rasol) =
+              case rasol of
+                Bad _ -> accu
+                Ok sol | null (asSolutions sol) -> accu
+                       | unallocable gdx -> accu
+                       | otherwise -> (gdx, sol):accu
+
+-- | Sort multigroup results based on policy and score.
+sortMGResults :: Group.List
+             -> [(Gdx, AllocSolution)]
+             -> [(Gdx, AllocSolution)]
+sortMGResults gl sols =
+    let extractScore (_, _, _, x) = x
+        solScore (gdx, sol) = (Group.allocPolicy (Container.find gdx gl),
+                               (extractScore . head . asSolutions) sol)
+    in sortBy (comparing solScore) sols
+
+-- | Finds the best group for an instance on a multi-group cluster.
+--
+-- Only solutions in @preferred@ and @last_resort@ groups will be
+-- accepted as valid, and additionally if the allowed groups parameter
+-- is not null then allocation will only be run for those group
+-- indices.
+findBestAllocGroup :: Group.List           -- ^ The group list
+                   -> Node.List            -- ^ The node list
+                   -> Instance.List        -- ^ The instance list
+                   -> Maybe [Gdx]          -- ^ The allowed groups
+                   -> Instance.Instance    -- ^ The instance to allocate
+                   -> Int                  -- ^ Required number of nodes
+                   -> Result (Gdx, AllocSolution, [String])
+findBestAllocGroup mggl mgnl mgil allowed_gdxs inst cnt =
+  let groups = splitCluster mgnl mgil
+      groups' = maybe groups (\gs -> filter ((`elem` gs) . fst) groups)
+                allowed_gdxs
+      sols = map (\(gid, (nl, il)) ->
+                   (gid, genAllocNodes mggl nl cnt False >>=
+                       tryAlloc nl il inst))
+             groups'::[(Gdx, Result AllocSolution)]
+      all_msgs = concatMap (solutionDescription mggl) sols
+      goodSols = filterMGResults mggl sols
+      sortedSols = sortMGResults mggl goodSols
+  in if null sortedSols
+     then Bad $ intercalate ", " all_msgs
+     else let (final_group, final_sol) = head sortedSols
+          in return (final_group, final_sol, all_msgs)
+
+-- | Try to allocate an instance on a multi-group cluster.
+tryMGAlloc :: Group.List           -- ^ The group list
+           -> Node.List            -- ^ The node list
+           -> Instance.List        -- ^ The instance list
+           -> Instance.Instance    -- ^ The instance to allocate
+           -> Int                  -- ^ Required number of nodes
+           -> Result AllocSolution -- ^ Possible solution list
+tryMGAlloc mggl mgnl mgil inst cnt = do
+  (best_group, solution, all_msgs) <-
+      findBestAllocGroup mggl mgnl mgil Nothing inst cnt
+  let group_name = Group.name $ Container.find best_group mggl
+      selmsg = "Selected group: " ++ group_name
+  return $ solution { asLog = selmsg:all_msgs }
+
+-- | Try to relocate an instance on the cluster.
+tryReloc :: (Monad m) =>
+            Node.List       -- ^ The node list
+         -> Instance.List   -- ^ The instance list
+         -> Idx             -- ^ The index of the instance to move
+         -> Int             -- ^ The number of nodes required
+         -> [Ndx]           -- ^ Nodes which should not be used
+         -> m AllocSolution -- ^ Solution list
+tryReloc nl il xid 1 ex_idx =
+    let all_nodes = getOnline nl
+        inst = Container.find xid il
+        ex_idx' = Instance.pNode inst:ex_idx
+        valid_nodes = filter (not . flip elem ex_idx' . Node.idx) all_nodes
+        valid_idxes = map Node.idx valid_nodes
+        sols1 = foldl' (\cstate x ->
+                            let em = do
+                                  (mnl, i, _, _) <-
+                                      applyMove nl inst (ReplaceSecondary x)
+                                  return (mnl, i, [Container.find x mnl],
+                                          compCV mnl)
+                            in concatAllocs cstate em
+                       ) emptyAllocSolution valid_idxes
+    in return sols1
+
+tryReloc _ _ _ reqn _  = fail $ "Unsupported number of relocation \
+                                \destinations required (" ++ show reqn ++
+                                                  "), only one supported"
+
+-- | Change an instance's secondary node.
+evacInstance :: (Monad m) =>
+                [Ndx]                      -- ^ Excluded nodes
+             -> Instance.List              -- ^ The current instance list
+             -> (Node.List, AllocSolution) -- ^ The current state
+             -> Idx                        -- ^ The instance to evacuate
+             -> m (Node.List, AllocSolution)
+evacInstance ex_ndx il (nl, old_as) idx = do
+  -- FIXME: hardcoded one node here
+
+  -- Longer explanation: evacuation is currently hardcoded to DRBD
+  -- instances (which have one secondary); hence, even if the
+  -- IAllocator protocol can request N nodes for an instance, and all
+  -- the message parsing/loading pass this, this implementation only
+  -- supports one; this situation needs to be revisited if we ever
+  -- support more than one secondary, or if we change the storage
+  -- model
+  new_as <- tryReloc nl il idx 1 ex_ndx
+  case asSolutions new_as of
+    -- an individual relocation succeeded, we kind of compose the data
+    -- from the two solutions
+    csol@(nl', _, _, _):_ ->
+        return (nl', new_as { asSolutions = csol:asSolutions old_as })
+    -- this relocation failed, so we fail the entire evac
+    _ -> fail $ "Can't evacuate instance " ++
+         Instance.name (Container.find idx il) ++
+             ": " ++ describeSolution new_as
+
+-- | Try to evacuate a list of nodes.
+tryEvac :: (Monad m) =>
+            Node.List       -- ^ The node list
+         -> Instance.List   -- ^ The instance list
+         -> [Idx]           -- ^ Instances to be evacuated
+         -> [Ndx]           -- ^ Restricted nodes (the ones being evacuated)
+         -> m AllocSolution -- ^ Solution list
+tryEvac nl il idxs ex_ndx = do
+  (_, sol) <- foldM (evacInstance ex_ndx il) (nl, emptyAllocSolution) idxs
+  return sol
+
+-- | Function which fails if the requested mode is change secondary.
+--
+-- This is useful since except DRBD, no other disk template can
+-- execute change secondary; thus, we can just call this function
+-- instead of always checking for secondary mode. After the call to
+-- this function, whatever mode we have is just a primary change.
+failOnSecondaryChange :: (Monad m) => EvacMode -> DiskTemplate -> m ()
+failOnSecondaryChange ChangeSecondary dt =
+    fail $ "Instances with disk template '" ++ dtToString dt ++
+         "' can't execute change secondary"
+failOnSecondaryChange _ _ = return ()
+
+-- | Run evacuation for a single instance.
+--
+-- /Note:/ this function should correctly execute both intra-group
+-- evacuations (in all modes) and inter-group evacuations (in the
+-- 'ChangeAll' mode). Of course, this requires that the correct list
+-- of target nodes is passed.
+nodeEvacInstance :: Node.List         -- ^ The node list (cluster-wide)
+                 -> Instance.List     -- ^ Instance list (cluster-wide)
+                 -> EvacMode          -- ^ The evacuation mode
+                 -> Instance.Instance -- ^ The instance to be evacuated
+                 -> Gdx               -- ^ The group we're targetting
+                 -> [Ndx]             -- ^ The list of available nodes
+                                      -- for allocation
+                 -> Result (Node.List, Instance.List, [OpCodes.OpCode])
+nodeEvacInstance _ _ mode (Instance.Instance
+                           {Instance.diskTemplate = dt@DTDiskless}) _ _ =
+                  failOnSecondaryChange mode dt >>
+                  fail "Diskless relocations not implemented yet"
+
+nodeEvacInstance _ _ _ (Instance.Instance
+                        {Instance.diskTemplate = DTPlain}) _ _ =
+                  fail "Instances of type plain cannot be relocated"
+
+nodeEvacInstance _ _ _ (Instance.Instance
+                        {Instance.diskTemplate = DTFile}) _ _ =
+                  fail "Instances of type file cannot be relocated"
+
+nodeEvacInstance _ _ mode  (Instance.Instance
+                            {Instance.diskTemplate = dt@DTSharedFile}) _ _ =
+                  failOnSecondaryChange mode dt >>
+                  fail "Shared file relocations not implemented yet"
+
+nodeEvacInstance _ _ mode (Instance.Instance
+                           {Instance.diskTemplate = dt@DTBlock}) _ _ =
+                  failOnSecondaryChange mode dt >>
+                  fail "Block device relocations not implemented yet"
+
+nodeEvacInstance nl il ChangePrimary
+                 inst@(Instance.Instance {Instance.diskTemplate = DTDrbd8})
+                 _ _ =
+  do
+    (nl', inst', _, _) <- opToResult $ applyMove nl inst Failover
+    let idx = Instance.idx inst
+        il' = Container.add idx inst' il
+        ops = iMoveToJob nl' il' idx Failover
+    return (nl', il', ops)
+
+nodeEvacInstance nl il ChangeSecondary
+                 inst@(Instance.Instance {Instance.diskTemplate = DTDrbd8})
+                 gdx avail_nodes =
+  do
+    (nl', inst', _, ndx) <- annotateResult "Can't find any good node" $
+                            eitherToResult $
+                            foldl' (evacDrbdSecondaryInner nl inst gdx)
+                            (Left "no nodes available") avail_nodes
+    let idx = Instance.idx inst
+        il' = Container.add idx inst' il
+        ops = iMoveToJob nl' il' idx (ReplaceSecondary ndx)
+    return (nl', il', ops)
+
+-- The algorithm for ChangeAll is as follows:
+--
+-- * generate all (primary, secondary) node pairs for the target groups
+-- * for each pair, execute the needed moves (r:s, f, r:s) and compute
+--   the final node list state and group score
+-- * select the best choice via a foldl that uses the same Either
+--   String solution as the ChangeSecondary mode
+nodeEvacInstance nl il ChangeAll
+                 inst@(Instance.Instance {Instance.diskTemplate = DTDrbd8})
+                 gdx avail_nodes =
+  do
+    let no_nodes = Left "no nodes available"
+        node_pairs = [(p,s) | p <- avail_nodes, s <- avail_nodes, p /= s]
+    (nl', il', ops, _) <-
+        annotateResult "Can't find any good nodes for relocation" $
+        eitherToResult $
+        foldl'
+        (\accu nodes -> case evacDrbdAllInner nl il inst gdx nodes of
+                          Bad msg ->
+                              case accu of
+                                Right _ -> accu
+                                -- we don't need more details (which
+                                -- nodes, etc.) as we only selected
+                                -- this group if we can allocate on
+                                -- it, hence failures will not
+                                -- propagate out of this fold loop
+                                Left _ -> Left $ "Allocation failed: " ++ msg
+                          Ok result@(_, _, _, new_cv) ->
+                              let new_accu = Right result in
+                              case accu of
+                                Left _ -> new_accu
+                                Right (_, _, _, old_cv) ->
+                                    if old_cv < new_cv
+                                    then accu
+                                    else new_accu
+        ) no_nodes node_pairs
+
+    return (nl', il', ops)
+
+-- | Inner fold function for changing secondary of a DRBD instance.
+--
+-- The running solution is either a @Left String@, which means we
+-- don't have yet a working solution, or a @Right (...)@, which
+-- represents a valid solution; it holds the modified node list, the
+-- modified instance (after evacuation), the score of that solution,
+-- and the new secondary node index.
+evacDrbdSecondaryInner :: Node.List -- ^ Cluster node list
+                       -> Instance.Instance -- ^ Instance being evacuated
+                       -> Gdx -- ^ The group index of the instance
+                       -> Either String ( Node.List
+                                        , Instance.Instance
+                                        , Score
+                                        , Ndx)  -- ^ Current best solution
+                       -> Ndx  -- ^ Node we're evaluating as new secondary
+                       -> Either String ( Node.List
+                                        , Instance.Instance
+                                        , Score
+                                        , Ndx) -- ^ New best solution
+evacDrbdSecondaryInner nl inst gdx accu ndx =
+    case applyMove nl inst (ReplaceSecondary ndx) of
+      OpFail fm ->
+          case accu of
+            Right _ -> accu
+            Left _ -> Left $ "Node " ++ Container.nameOf nl ndx ++
+                      " failed: " ++ show fm
+      OpGood (nl', inst', _, _) ->
+          let nodes = Container.elems nl'
+              -- The fromJust below is ugly (it can fail nastily), but
+              -- at this point we should have any internal mismatches,
+              -- and adding a monad here would be quite involved
+              grpnodes = fromJust (gdx `lookup` Node.computeGroups nodes)
+              new_cv = compCVNodes grpnodes
+              new_accu = Right (nl', inst', new_cv, ndx)
+          in case accu of
+               Left _ -> new_accu
+               Right (_, _, old_cv, _) ->
+                   if old_cv < new_cv
+                   then accu
+                   else new_accu
+
+-- | Compute result of changing all nodes of a DRBD instance.
+--
+-- Given the target primary and secondary node (which might be in a
+-- different group or not), this function will 'execute' all the
+-- required steps and assuming all operations succceed, will return
+-- the modified node and instance lists, the opcodes needed for this
+-- and the new group score.
+evacDrbdAllInner :: Node.List         -- ^ Cluster node list
+                 -> Instance.List     -- ^ Cluster instance list
+                 -> Instance.Instance -- ^ The instance to be moved
+                 -> Gdx               -- ^ The target group index
+                                      -- (which can differ from the
+                                      -- current group of the
+                                      -- instance)
+                 -> (Ndx, Ndx)        -- ^ Tuple of new
+                                      -- primary\/secondary nodes
+                 -> Result (Node.List, Instance.List, [OpCodes.OpCode], Score)
+evacDrbdAllInner nl il inst gdx (t_pdx, t_sdx) =
+  do
+    let primary = Container.find (Instance.pNode inst) nl
+        idx = Instance.idx inst
+    -- if the primary is offline, then we first failover
+    (nl1, inst1, ops1) <-
+        if Node.offline primary
+        then do
+          (nl', inst', _, _) <-
+              annotateResult "Failing over to the secondary" $
+              opToResult $ applyMove nl inst Failover
+          return (nl', inst', [Failover])
+        else return (nl, inst, [])
+    let (o1, o2, o3) = (ReplaceSecondary t_pdx,
+                        Failover,
+                        ReplaceSecondary t_sdx)
+    -- we now need to execute a replace secondary to the future
+    -- primary node
+    (nl2, inst2, _, _) <-
+        annotateResult "Changing secondary to new primary" $
+        opToResult $
+        applyMove nl1 inst1 o1
+    let ops2 = o1:ops1
+    -- we now execute another failover, the primary stays fixed now
+    (nl3, inst3, _, _) <- annotateResult "Failing over to new primary" $
+                          opToResult $ applyMove nl2 inst2 o2
+    let ops3 = o2:ops2
+    -- and finally another replace secondary, to the final secondary
+    (nl4, inst4, _, _) <-
+        annotateResult "Changing secondary to final secondary" $
+        opToResult $
+        applyMove nl3 inst3 o3
+    let ops4 = o3:ops3
+        il' = Container.add idx inst4 il
+        ops = concatMap (iMoveToJob nl4 il' idx) $ reverse ops4
+    let nodes = Container.elems nl4
+        -- The fromJust below is ugly (it can fail nastily), but
+        -- at this point we should have any internal mismatches,
+        -- and adding a monad here would be quite involved
+        grpnodes = fromJust (gdx `lookup` Node.computeGroups nodes)
+        new_cv = compCVNodes grpnodes
+    return (nl4, il', ops, new_cv)
+
+-- | Computes the nodes in a given group which are available for
+-- allocation.
+availableGroupNodes :: [(Gdx, [Ndx])] -- ^ Group index/node index assoc list
+                    -> IntSet.IntSet  -- ^ Nodes that are excluded
+                    -> Gdx            -- ^ The group for which we
+                                      -- query the nodes
+                    -> Result [Ndx]   -- ^ List of available node indices
+availableGroupNodes group_nodes excl_ndx gdx = do
+  local_nodes <- maybe (Bad $ "Can't find group with index " ++ show gdx)
+                 Ok (lookup gdx group_nodes)
+  let avail_nodes = filter (not . flip IntSet.member excl_ndx) local_nodes
+  return avail_nodes
+
+-- | Updates the evac solution with the results of an instance
+-- evacuation.
+updateEvacSolution :: (Node.List, Instance.List, EvacSolution)
+                   -> Idx
+                   -> Result (Node.List, Instance.List, [OpCodes.OpCode])
+                   -> (Node.List, Instance.List, EvacSolution)
+updateEvacSolution (nl, il, es) idx (Bad msg) =
+    (nl, il, es { esFailed = (idx, msg):esFailed es})
+updateEvacSolution (_, _, es) idx (Ok (nl, il, opcodes)) =
+    (nl, il, es { esMoved = new_elem:esMoved es
+                , esOpCodes = [opcodes]:esOpCodes es })
+     where inst = Container.find idx il
+           new_elem = (idx,
+                       instancePriGroup nl inst,
+                       Instance.allNodes inst)
+
+-- | Node-evacuation IAllocator mode main function.
+tryNodeEvac :: Group.List    -- ^ The cluster groups
+            -> Node.List     -- ^ The node list (cluster-wide, not per group)
+            -> Instance.List -- ^ Instance list (cluster-wide)
+            -> EvacMode      -- ^ The evacuation mode
+            -> [Idx]         -- ^ List of instance (indices) to be evacuated
+            -> Result (Node.List, Instance.List, EvacSolution)
+tryNodeEvac _ ini_nl ini_il mode idxs =
+    let evac_ndx = nodesToEvacuate ini_il mode idxs
+        offline = map Node.idx . filter Node.offline $ Container.elems ini_nl
+        excl_ndx = foldl' (flip IntSet.insert) evac_ndx offline
+        group_ndx = map (\(gdx, (nl, _)) -> (gdx, map Node.idx
+                                             (Container.elems nl))) $
+                      splitCluster ini_nl ini_il
+        (fin_nl, fin_il, esol) =
+            foldl' (\state@(nl, il, _) inst ->
+                        let gdx = instancePriGroup nl inst in
+                        updateEvacSolution state (Instance.idx inst) $
+                        availableGroupNodes group_ndx
+                          excl_ndx gdx >>=
+                        nodeEvacInstance nl il mode inst gdx
+                   )
+            (ini_nl, ini_il, emptyEvacSolution)
+            (map (`Container.find` ini_il) idxs)
+    in return (fin_nl, fin_il, reverseEvacSolution esol)
+
+-- | Change-group IAllocator mode main function.
+--
+-- This is very similar to 'tryNodeEvac', the only difference is that
+-- we don't choose as target group the current instance group, but
+-- instead:
+--
+--   1. at the start of the function, we compute which are the target
+--   groups; either no groups were passed in, in which case we choose
+--   all groups out of which we don't evacuate instance, or there were
+--   some groups passed, in which case we use those
+--
+--   2. for each instance, we use 'findBestAllocGroup' to choose the
+--   best group to hold the instance, and then we do what
+--   'tryNodeEvac' does, except for this group instead of the current
+--   instance group.
+--
+-- Note that the correct behaviour of this function relies on the
+-- function 'nodeEvacInstance' to be able to do correctly both
+-- intra-group and inter-group moves when passed the 'ChangeAll' mode.
+tryChangeGroup :: Group.List    -- ^ The cluster groups
+               -> Node.List     -- ^ The node list (cluster-wide)
+               -> Instance.List -- ^ Instance list (cluster-wide)
+               -> [Gdx]         -- ^ Target groups; if empty, any
+                                -- groups not being evacuated
+               -> [Idx]         -- ^ List of instance (indices) to be evacuated
+               -> Result (Node.List, Instance.List, EvacSolution)
+tryChangeGroup gl ini_nl ini_il gdxs idxs =
+    let evac_gdxs = nub $ map (instancePriGroup ini_nl .
+                               flip Container.find ini_il) idxs
+        target_gdxs = (if null gdxs
+                       then Container.keys gl
+                       else gdxs) \\ evac_gdxs
+        offline = map Node.idx . filter Node.offline $ Container.elems ini_nl
+        excl_ndx = foldl' (flip IntSet.insert) IntSet.empty offline
+        group_ndx = map (\(gdx, (nl, _)) -> (gdx, map Node.idx
+                                             (Container.elems nl))) $
+                      splitCluster ini_nl ini_il
+        (fin_nl, fin_il, esol) =
+            foldl' (\state@(nl, il, _) inst ->
+                        let solution = do
+                              let ncnt = Instance.requiredNodes $
+                                         Instance.diskTemplate inst
+                              (gdx, _, _) <- findBestAllocGroup gl nl il
+                                             (Just target_gdxs) inst ncnt
+                              av_nodes <- availableGroupNodes group_ndx
+                                          excl_ndx gdx
+                              nodeEvacInstance nl il ChangeAll inst
+                                       gdx av_nodes
+                        in updateEvacSolution state
+                               (Instance.idx inst) solution
+                   )
+            (ini_nl, ini_il, emptyEvacSolution)
+            (map (`Container.find` ini_il) idxs)
+    in return (fin_nl, fin_il, reverseEvacSolution esol)
+
+-- | Recursively place instances on the cluster until we're out of space.
+iterateAlloc :: Node.List
+             -> Instance.List
+             -> Maybe Int
+             -> Instance.Instance
+             -> AllocNodes
+             -> [Instance.Instance]
+             -> [CStats]
+             -> Result AllocResult
+iterateAlloc nl il limit newinst allocnodes ixes cstats =
+      let depth = length ixes
+          newname = printf "new-%d" depth::String
+          newidx = length (Container.elems il) + depth
+          newi2 = Instance.setIdx (Instance.setName newinst newname) newidx
+          newlimit = fmap (flip (-) 1) limit
+      in case tryAlloc nl il newi2 allocnodes of
+           Bad s -> Bad s
+           Ok (AllocSolution { asFailures = errs, asSolutions = sols3 }) ->
+               let newsol = Ok (collapseFailures errs, nl, il, ixes, cstats) in
+               case sols3 of
+                 [] -> newsol
+                 (xnl, xi, _, _):[] ->
+                     if limit == Just 0
+                     then newsol
+                     else iterateAlloc xnl (Container.add newidx xi il)
+                          newlimit newinst allocnodes (xi:ixes)
+                          (totalResources xnl:cstats)
+                 _ -> Bad "Internal error: multiple solutions for single\
+                          \ allocation"
+
+-- | The core of the tiered allocation mode.
+tieredAlloc :: Node.List
+            -> Instance.List
+            -> Maybe Int
+            -> Instance.Instance
+            -> AllocNodes
+            -> [Instance.Instance]
+            -> [CStats]
+            -> Result AllocResult
+tieredAlloc nl il limit newinst allocnodes ixes cstats =
+    case iterateAlloc nl il limit newinst allocnodes ixes cstats of
+      Bad s -> Bad s
+      Ok (errs, nl', il', ixes', cstats') ->
+          let newsol = Ok (errs, nl', il', ixes', cstats')
+              ixes_cnt = length ixes'
+              (stop, newlimit) = case limit of
+                                   Nothing -> (False, Nothing)
+                                   Just n -> (n <= ixes_cnt,
+                                              Just (n - ixes_cnt)) in
+          if stop then newsol else
+          case Instance.shrinkByType newinst . fst . last $
+               sortBy (comparing snd) errs of
+            Bad _ -> newsol
+            Ok newinst' -> tieredAlloc nl' il' newlimit
+                           newinst' allocnodes ixes' cstats'
+
+-- * Formatting functions
+
+-- | Given the original and final nodes, computes the relocation description.
+computeMoves :: Instance.Instance -- ^ The instance to be moved
+             -> String -- ^ The instance name
+             -> IMove  -- ^ The move being performed
+             -> String -- ^ New primary
+             -> String -- ^ New secondary
+             -> (String, [String])
+                -- ^ Tuple of moves and commands list; moves is containing
+                -- either @/f/@ for failover or @/r:name/@ for replace
+                -- secondary, while the command list holds gnt-instance
+                -- commands (without that prefix), e.g \"@failover instance1@\"
+computeMoves i inam mv c d =
+    case mv of
+      Failover -> ("f", [mig])
+      FailoverAndReplace _ -> (printf "f r:%s" d, [mig, rep d])
+      ReplaceSecondary _ -> (printf "r:%s" d, [rep d])
+      ReplaceAndFailover _ -> (printf "r:%s f" c, [rep c, mig])
+      ReplacePrimary _ -> (printf "f r:%s f" c, [mig, rep c, mig])
+    where morf = if Instance.running i then "migrate" else "failover"
+          mig = printf "%s -f %s" morf inam::String
+          rep n = printf "replace-disks -n %s %s" n inam
+
+-- | Converts a placement to string format.
+printSolutionLine :: Node.List     -- ^ The node list
+                  -> Instance.List -- ^ The instance list
+                  -> Int           -- ^ Maximum node name length
+                  -> Int           -- ^ Maximum instance name length
+                  -> Placement     -- ^ The current placement
+                  -> Int           -- ^ The index of the placement in
+                                   -- the solution
+                  -> (String, [String])
+printSolutionLine nl il nmlen imlen plc pos =
+    let
+        pmlen = (2*nmlen + 1)
+        (i, p, s, mv, c) = plc
+        inst = Container.find i il
+        inam = Instance.alias inst
+        npri = Node.alias $ Container.find p nl
+        nsec = Node.alias $ Container.find s nl
+        opri = Node.alias $ Container.find (Instance.pNode inst) nl
+        osec = Node.alias $ Container.find (Instance.sNode inst) nl
+        (moves, cmds) =  computeMoves inst inam mv npri nsec
+        ostr = printf "%s:%s" opri osec::String
+        nstr = printf "%s:%s" npri nsec::String
+    in
+      (printf "  %3d. %-*s %-*s => %-*s %.8f a=%s"
+       pos imlen inam pmlen ostr
+       pmlen nstr c moves,
+       cmds)
+
+-- | Return the instance and involved nodes in an instance move.
+--
+-- Note that the output list length can vary, and is not required nor
+-- guaranteed to be of any specific length.
+involvedNodes :: Instance.List -- ^ Instance list, used for retrieving
+                               -- the instance from its index; note
+                               -- that this /must/ be the original
+                               -- instance list, so that we can
+                               -- retrieve the old nodes
+              -> Placement     -- ^ The placement we're investigating,
+                               -- containing the new nodes and
+                               -- instance index
+              -> [Ndx]         -- ^ Resulting list of node indices
+involvedNodes il plc =
+    let (i, np, ns, _, _) = plc
+        inst = Container.find i il
+    in nub $ [np, ns] ++ Instance.allNodes inst
+
+-- | Inner function for splitJobs, that either appends the next job to
+-- the current jobset, or starts a new jobset.
+mergeJobs :: ([JobSet], [Ndx]) -> MoveJob -> ([JobSet], [Ndx])
+mergeJobs ([], _) n@(ndx, _, _, _) = ([[n]], ndx)
+mergeJobs (cjs@(j:js), nbuf) n@(ndx, _, _, _)
+    | null (ndx `intersect` nbuf) = ((n:j):js, ndx ++ nbuf)
+    | otherwise = ([n]:cjs, ndx)
+
+-- | Break a list of moves into independent groups. Note that this
+-- will reverse the order of jobs.
+splitJobs :: [MoveJob] -> [JobSet]
+splitJobs = fst . foldl mergeJobs ([], [])
+
+-- | Given a list of commands, prefix them with @gnt-instance@ and
+-- also beautify the display a little.
+formatJob :: Int -> Int -> (Int, MoveJob) -> [String]
+formatJob jsn jsl (sn, (_, _, _, cmds)) =
+    let out =
+            printf "  echo job %d/%d" jsn sn:
+            printf "  check":
+            map ("  gnt-instance " ++) cmds
+    in if sn == 1
+       then ["", printf "echo jobset %d, %d jobs" jsn jsl] ++ out
+       else out
+
+-- | Given a list of commands, prefix them with @gnt-instance@ and
+-- also beautify the display a little.
+formatCmds :: [JobSet] -> String
+formatCmds =
+    unlines .
+    concatMap (\(jsn, js) -> concatMap (formatJob jsn (length js))
+                             (zip [1..] js)) .
+    zip [1..]
+
+-- | Print the node list.
+printNodes :: Node.List -> [String] -> String
+printNodes nl fs =
+    let fields = case fs of
+          [] -> Node.defaultFields
+          "+":rest -> Node.defaultFields ++ rest
+          _ -> fs
+        snl = sortBy (comparing Node.idx) (Container.elems nl)
+        (header, isnum) = unzip $ map Node.showHeader fields
+    in unlines . map ((:) ' ' .  intercalate " ") $
+       formatTable (header:map (Node.list fields) snl) isnum
+
+-- | Print the instance list.
+printInsts :: Node.List -> Instance.List -> String
+printInsts nl il =
+    let sil = sortBy (comparing Instance.idx) (Container.elems il)
+        helper inst = [ if Instance.running inst then "R" else " "
+                      , Instance.name inst
+                      , Container.nameOf nl (Instance.pNode inst)
+                      , let sdx = Instance.sNode inst
+                        in if sdx == Node.noSecondary
+                           then  ""
+                           else Container.nameOf nl sdx
+                      , if Instance.autoBalance inst then "Y" else "N"
+                      , printf "%3d" $ Instance.vcpus inst
+                      , printf "%5d" $ Instance.mem inst
+                      , printf "%5d" $ Instance.dsk inst `div` 1024
+                      , printf "%5.3f" lC
+                      , printf "%5.3f" lM
+                      , printf "%5.3f" lD
+                      , printf "%5.3f" lN
+                      ]
+            where DynUtil lC lM lD lN = Instance.util inst
+        header = [ "F", "Name", "Pri_node", "Sec_node", "Auto_bal"
+                 , "vcpu", "mem" , "dsk", "lCpu", "lMem", "lDsk", "lNet" ]
+        isnum = False:False:False:False:False:repeat True
+    in unlines . map ((:) ' ' . intercalate " ") $
+       formatTable (header:map helper sil) isnum
+
+-- | Shows statistics for a given node list.
+printStats :: Node.List -> String
+printStats nl =
+    let dcvs = compDetailedCV $ Container.elems nl
+        (weights, names) = unzip detailedCVInfo
+        hd = zip3 (weights ++ repeat 1) (names ++ repeat "unknown") dcvs
+        formatted = map (\(w, header, val) ->
+                             printf "%s=%.8f(x%.2f)" header val w::String) hd
+    in intercalate ", " formatted
+
+-- | Convert a placement into a list of OpCodes (basically a job).
+iMoveToJob :: Node.List        -- ^ The node list; only used for node
+                               -- names, so any version is good
+                               -- (before or after the operation)
+           -> Instance.List    -- ^ The instance list; also used for
+                               -- names only
+           -> Idx              -- ^ The index of the instance being
+                               -- moved
+           -> IMove            -- ^ The actual move to be described
+           -> [OpCodes.OpCode] -- ^ The list of opcodes equivalent to
+                               -- the given move
+iMoveToJob nl il idx move =
+    let inst = Container.find idx il
+        iname = Instance.name inst
+        lookNode  = Just . Container.nameOf nl
+        opF = OpCodes.OpInstanceMigrate iname True False True Nothing
+        opR n = OpCodes.OpInstanceReplaceDisks iname (lookNode n)
+                OpCodes.ReplaceNewSecondary [] Nothing
+    in case move of
+         Failover -> [ opF ]
+         ReplacePrimary np -> [ opF, opR np, opF ]
+         ReplaceSecondary ns -> [ opR ns ]
+         ReplaceAndFailover np -> [ opR np, opF ]
+         FailoverAndReplace ns -> [ opF, opR ns ]
+
+-- * Node group functions
+
+-- | Computes the group of an instance.
+instanceGroup :: Node.List -> Instance.Instance -> Result Gdx
+instanceGroup nl i =
+  let sidx = Instance.sNode i
+      pnode = Container.find (Instance.pNode i) nl
+      snode = if sidx == Node.noSecondary
+              then pnode
+              else Container.find sidx nl
+      pgroup = Node.group pnode
+      sgroup = Node.group snode
+  in if pgroup /= sgroup
+     then fail ("Instance placed accross two node groups, primary " ++
+                show pgroup ++ ", secondary " ++ show sgroup)
+     else return pgroup
+
+-- | Computes the group of an instance per the primary node.
+instancePriGroup :: Node.List -> Instance.Instance -> Gdx
+instancePriGroup nl i =
+  let pnode = Container.find (Instance.pNode i) nl
+  in  Node.group pnode
+
+-- | Compute the list of badly allocated instances (split across node
+-- groups).
+findSplitInstances :: Node.List -> Instance.List -> [Instance.Instance]
+findSplitInstances nl =
+  filter (not . isOk . instanceGroup nl) . Container.elems
+
+-- | Splits a cluster into the component node groups.
+splitCluster :: Node.List -> Instance.List ->
+                [(Gdx, (Node.List, Instance.List))]
+splitCluster nl il =
+  let ngroups = Node.computeGroups (Container.elems nl)
+  in map (\(guuid, nodes) ->
+           let nidxs = map Node.idx nodes
+               nodes' = zip nidxs nodes
+               instances = Container.filter ((`elem` nidxs) . Instance.pNode) il
+           in (guuid, (Container.fromList nodes', instances))) ngroups
+
+-- | Compute the list of nodes that are to be evacuated, given a list
+-- of instances and an evacuation mode.
+nodesToEvacuate :: Instance.List -- ^ The cluster-wide instance list
+                -> EvacMode      -- ^ The evacuation mode we're using
+                -> [Idx]         -- ^ List of instance indices being evacuated
+                -> IntSet.IntSet -- ^ Set of node indices
+nodesToEvacuate il mode =
+    IntSet.delete Node.noSecondary .
+    foldl' (\ns idx ->
+                let i = Container.find idx il
+                    pdx = Instance.pNode i
+                    sdx = Instance.sNode i
+                    dt = Instance.diskTemplate i
+                    withSecondary = case dt of
+                                      DTDrbd8 -> IntSet.insert sdx ns
+                                      _ -> ns
+                in case mode of
+                     ChangePrimary   -> IntSet.insert pdx ns
+                     ChangeSecondary -> withSecondary
+                     ChangeAll       -> IntSet.insert pdx withSecondary
+           ) IntSet.empty
diff --git a/htools/Ganeti/HTools/Compat.hs b/htools/Ganeti/HTools/Compat.hs
new file mode 100644 (file)
index 0000000..36a0fbf
--- /dev/null
@@ -0,0 +1,47 @@
+{-# LANGUAGE CPP #-}
+
+{- | Compatibility helper module.
+
+This module holds definitions that help with supporting multiple library versions or transitions between versions.
+
+-}
+
+{-
+
+Copyright (C) 2011 Google Inc.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+-}
+
+module Ganeti.HTools.Compat
+    ( rwhnf
+    , Control.Parallel.Strategies.parMap
+    ) where
+
+import qualified Control.Parallel.Strategies
+
+-- | Wrapper over the function exported from
+-- "Control.Parallel.Strategies".
+--
+-- This wraps either the old or the new name of the function,
+-- depending on the detected library version.
+rwhnf :: Control.Parallel.Strategies.Strategy a
+#ifdef PARALLEL3
+rwhnf = Control.Parallel.Strategies.rseq
+#else
+rwhnf = Control.Parallel.Strategies.rwhnf
+#endif
diff --git a/htools/Ganeti/HTools/Container.hs b/htools/Ganeti/HTools/Container.hs
new file mode 100644 (file)
index 0000000..5b2d3cc
--- /dev/null
@@ -0,0 +1,93 @@
+{-| Module abstracting the node and instance container implementation.
+
+This is currently implemented on top of an 'IntMap', which seems to
+give the best performance for our workload.
+
+-}
+
+{-
+
+Copyright (C) 2009, 2010, 2011 Google Inc.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+-}
+
+module Ganeti.HTools.Container
+    (
+     -- * Types
+     Container
+    , Key
+     -- * Creation
+    , IntMap.empty
+    , IntMap.singleton
+    , IntMap.fromList
+     -- * Query
+    , IntMap.size
+    , IntMap.null
+    , find
+    , IntMap.findMax
+    , IntMap.member
+     -- * Update
+    , add
+    , addTwo
+    , IntMap.map
+    , IntMap.mapAccum
+    , IntMap.filter
+    -- * Conversion
+    , IntMap.elems
+    , IntMap.keys
+    -- * Element functions
+    , nameOf
+    , findByName
+    ) where
+
+import qualified Data.IntMap as IntMap
+
+import qualified Ganeti.HTools.Types as T
+
+-- | Our key type.
+
+type Key = IntMap.Key
+
+-- | Our container type.
+type Container = IntMap.IntMap
+
+-- | Locate a key in the map (must exist).
+find :: Key -> Container a -> a
+find k = (IntMap.! k)
+
+-- | Add or update one element to the map.
+add :: Key -> a -> Container a -> Container a
+add = IntMap.insert
+
+-- | Add or update two elements of the map.
+addTwo :: Key -> a -> Key -> a -> Container a -> Container a
+addTwo k1 v1 k2 v2 = add k1 v1 . add k2 v2
+
+-- | Compute the name of an element in a container.
+nameOf :: (T.Element a) => Container a -> Key -> String
+nameOf c k = T.nameOf $ find k c
+
+-- | Find an element by name in a Container; this is a very slow function.
+findByName :: (T.Element a, Monad m) =>
+              Container a -> String -> m a
+findByName c n =
+    let all_elems = IntMap.elems c
+        result = filter ((n `elem`) . T.allNames) all_elems
+    in case result of
+         [item] -> return item
+         _ -> fail $ "Wrong number of elems found with name " ++ n
diff --git a/htools/Ganeti/HTools/ExtLoader.hs b/htools/Ganeti/HTools/ExtLoader.hs
new file mode 100644 (file)
index 0000000..0b63a2c
--- /dev/null
@@ -0,0 +1,143 @@
+{-| External data loader.
+
+This module holds the external data loading, and thus is the only one
+depending (via the specialized Text\/Rapi\/Luxi modules) on the actual
+libraries implementing the low-level protocols.
+
+-}
+
+{-
+
+Copyright (C) 2009, 2010, 2011 Google Inc.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+-}
+
+module Ganeti.HTools.ExtLoader
+    ( loadExternalData
+    , commonSuffix
+    , maybeSaveData
+    ) where
+
+import Control.Monad
+import Data.Maybe (isJust, fromJust)
+import System.FilePath
+import System.IO
+import System
+import Text.Printf (hPrintf)
+
+import qualified Ganeti.HTools.Luxi as Luxi
+import qualified Ganeti.HTools.Rapi as Rapi
+import qualified Ganeti.HTools.Simu as Simu
+import qualified Ganeti.HTools.Text as Text
+import Ganeti.HTools.Loader (mergeData, checkData, ClusterData(..)
+                            , commonSuffix)
+
+import Ganeti.HTools.Types
+import Ganeti.HTools.CLI
+import Ganeti.HTools.Utils (sepSplit, tryRead)
+
+-- | Error beautifier.
+wrapIO :: IO (Result a) -> IO (Result a)
+wrapIO = flip catch (return . Bad . show)
+
+-- | Parses a user-supplied utilisation string.
+parseUtilisation :: String -> Result (String, DynUtil)
+parseUtilisation line =
+    case sepSplit ' ' line of
+      [name, cpu, mem, dsk, net] ->
+          do
+            rcpu <- tryRead name cpu
+            rmem <- tryRead name mem
+            rdsk <- tryRead name dsk
+            rnet <- tryRead name net
+            let du = DynUtil { cpuWeight = rcpu, memWeight = rmem
+                             , dskWeight = rdsk, netWeight = rnet }
+            return (name, du)
+      _ -> Bad $ "Cannot parse line " ++ line
+
+-- | External tool data loader from a variety of sources.
+loadExternalData :: Options
+                 -> IO ClusterData
+loadExternalData opts = do
+  let mhost = optMaster opts
+      lsock = optLuxi opts
+      tfile = optDataFile opts
+      simdata = optNodeSim opts
+      setRapi = mhost /= ""
+      setLuxi = isJust lsock
+      setSim = (not . null) simdata
+      setFile = isJust tfile
+      allSet = filter id [setRapi, setLuxi, setFile]
+      exTags = case optExTags opts of
+                 Nothing -> []
+                 Just etl -> map (++ ":") etl
+      selInsts = optSelInst opts
+      exInsts = optExInst opts
+
+  when (length allSet > 1) $
+       do
+         hPutStrLn stderr ("Error: Only one of the rapi, luxi, and data" ++
+                           " files options should be given.")
+         exitWith $ ExitFailure 1
+
+  util_contents <- (case optDynuFile opts of
+                      Just path -> readFile path
+                      Nothing -> return "")
+  let util_data = mapM parseUtilisation $ lines util_contents
+  util_data' <- (case util_data of
+                   Ok x -> return x
+                   Bad y -> do
+                     hPutStrLn stderr ("Error: can't parse utilisation" ++
+                                       " data: " ++ show y)
+                     exitWith $ ExitFailure 1)
+  input_data <-
+      case () of
+        _ | setRapi -> wrapIO $ Rapi.loadData mhost
+          | setLuxi -> wrapIO $ Luxi.loadData $ fromJust lsock
+          | setSim -> Simu.loadData simdata
+          | setFile -> wrapIO $ Text.loadData $ fromJust tfile
+          | otherwise -> return $ Bad "No backend selected! Exiting."
+
+  let ldresult = input_data >>= mergeData util_data' exTags selInsts exInsts
+  cdata <-
+      (case ldresult of
+         Ok x -> return x
+         Bad s -> do
+           hPrintf stderr
+             "Error: failed to load data, aborting. Details:\n%s\n" s:: IO ()
+           exitWith $ ExitFailure 1
+      )
+  let (fix_msgs, nl) = checkData (cdNodes cdata) (cdInstances cdata)
+
+  unless (optVerbose opts == 0) $ maybeShowWarnings fix_msgs
+
+  return cdata {cdNodes = nl}
+
+-- | Function to save the cluster data to a file.
+maybeSaveData :: Maybe FilePath -- ^ The file prefix to save to
+              -> String         -- ^ The suffix (extension) to add
+              -> String         -- ^ Informational message
+              -> ClusterData    -- ^ The cluster data
+              -> IO ()
+maybeSaveData Nothing _ _ _ = return ()
+maybeSaveData (Just path) ext msg cdata = do
+  let adata = Text.serializeCluster cdata
+      out_path = path <.> ext
+  writeFile out_path adata
+  hPrintf stderr "The cluster state %s has been written to file '%s'\n"
+          msg out_path
diff --git a/htools/Ganeti/HTools/Group.hs b/htools/Ganeti/HTools/Group.hs
new file mode 100644 (file)
index 0000000..6df5f4c
--- /dev/null
@@ -0,0 +1,90 @@
+{-| Module describing a node group.
+
+-}
+
+{-
+
+Copyright (C) 2010, 2011 Google Inc.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+-}
+
+module Ganeti.HTools.Group
+    ( Group(..)
+    , List
+    , AssocList
+    -- * Constructor
+    , create
+    , setIdx
+    , isAllocable
+    ) where
+
+import qualified Ganeti.HTools.Container as Container
+
+import qualified Ganeti.HTools.Types as T
+
+-- * Type declarations
+
+-- | The node group type.
+data Group = Group
+    { name        :: String        -- ^ The node name
+    , uuid        :: T.GroupID     -- ^ The UUID of the group
+    , idx         :: T.Gdx         -- ^ Internal index for book-keeping
+    , allocPolicy :: T.AllocPolicy -- ^ The allocation policy for this group
+    } deriving (Show, Read, Eq)
+
+-- Note: we use the name as the alias, and the UUID as the official
+-- name
+instance T.Element Group where
+    nameOf     = uuid
+    idxOf      = idx
+    setAlias   = setName
+    setIdx     = setIdx
+    allNames n = [name n, uuid n]
+
+-- | A simple name for the int, node association list.
+type AssocList = [(T.Gdx, Group)]
+
+-- | A simple name for a node map.
+type List = Container.Container Group
+
+-- * Initialization functions
+
+-- | Create a new group.
+create :: String -> T.GroupID -> T.AllocPolicy -> Group
+create name_init id_init apol_init =
+    Group { name        = name_init
+          , uuid        = id_init
+          , allocPolicy = apol_init
+          , idx         = -1
+          }
+
+-- | Sets the group index.
+--
+-- This is used only during the building of the data structures.
+setIdx :: Group -> T.Gdx -> Group
+setIdx t i = t {idx = i}
+
+-- | Changes the alias.
+--
+-- This is used only during the building of the data structures.
+setName :: Group -> String -> Group
+setName t s = t { name = s }
+
+-- | Checks if a group is allocable.
+isAllocable :: Group -> Bool
+isAllocable = (/= T.AllocUnallocable) . allocPolicy
diff --git a/htools/Ganeti/HTools/IAlloc.hs b/htools/Ganeti/HTools/IAlloc.hs
new file mode 100644 (file)
index 0000000..43e070f
--- /dev/null
@@ -0,0 +1,283 @@
+{-| Implementation of the iallocator interface.
+
+-}
+
+{-
+
+Copyright (C) 2009, 2010, 2011 Google Inc.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+-}
+
+module Ganeti.HTools.IAlloc
+    ( readRequest
+    , runIAllocator
+    ) where
+
+import Data.Either ()
+import Data.Maybe (fromMaybe, isJust)
+import Data.List
+import Control.Monad
+import Text.JSON (JSObject, JSValue(JSArray),
+                  makeObj, encodeStrict, decodeStrict, fromJSObject, showJSON)
+import System (exitWith, ExitCode(..))
+import System.IO
+
+import qualified Ganeti.HTools.Cluster as Cluster
+import qualified Ganeti.HTools.Container as Container
+import qualified Ganeti.HTools.Group as Group
+import qualified Ganeti.HTools.Node as Node
+import qualified Ganeti.HTools.Instance as Instance
+import qualified Ganeti.Constants as C
+import Ganeti.HTools.CLI
+import Ganeti.HTools.Loader
+import Ganeti.HTools.ExtLoader (loadExternalData)
+import Ganeti.HTools.Utils
+import Ganeti.HTools.Types
+
+-- | Type alias for the result of an IAllocator call.
+type IAllocResult = (String, JSValue, Node.List, Instance.List)
+
+-- | Parse